- 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.
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" 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.
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
- State clearly that the device is not supported
- Confirm that nothing has been changed on the device
- Offer a direct path to uninstall
- Work offline — no network access required to display
- Not proceed to initialise any services, sensors, or background workers before this check
cameraserver system-wide, affecting every other app on the device.- Do not enumerate sensors speculatively on startup
- Do not bind to hardware services in the background unless actively in use
- Release camera and sensor resources in
onPause(), notonDestroy()
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) }
}✓ 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.
| Layer | Action | Effort |
|---|---|---|
| Manifest | Set minSdkVersion accurately | Low |
| Manifest | Declare uses-feature with correct required value | Low |
| Play Console | Exclude confirmed incompatible device models by name | Low |
| Runtime | Check compatibility before initialising any services | Medium |
| Runtime | Show a clear, actionable incompatibility dialog | Medium |
| Code hygiene | Never access hardware speculatively in background | Ongoing |
| Crash reporting | Monitor by device model and act on patterns | Ongoing |
A user whose device goes dark for three hours because your app fought with their camera driver does not come back.