← Back to use cases

Testing Android Apps Across OS Versions: Complete Guide

Android OS versions aren't backward-compatible—not entirely. Code that works on Android 13 might fail on Android 12. A feature that passes on Android 14 might crash on Android 11.

Each Android version introduces breaking changes: new permission models, deprecated APIs, changed behaviors, new features. Testing across OS versions is how you ensure your app works for all users, regardless of which version they're running.

The challenge: if you support Android 10 through Android 15, you could test on 6 different OS versions. Multiply that by device variations, and testing scope explodes. This guide explains how to test across OS versions strategically—catching compatibility issues without testing exhaustively.

Understanding Android OS Version Changes

Major Breaking Changes by Version:

Android 10 (2019):

  • Scoped storage (limited file access)
  • Background execution restrictions
  • Gesture navigation (on-screen buttons removed)
  • Package visibility changes

Android 11 (2020):

  • Hibernation of unused apps
  • More permission restrictions
  • Microphone/camera indicator system
  • One-time permissions

Android 12 (2021):

  • Approximate location permission
  • Bluetooth runtime permission
  • Clipboard access notification
  • Material You design system

Android 13 (2022):

  • Per-app language preferences
  • Stricter enforce of @hide APIs
  • Bluetooth scan permission
  • Credential management system

Android 14 (2023):

  • Partial media permissions
  • Grammatical gender inflection
  • Data safety requirement on Play Store
  • Restricted access to device identifiers

Android 15 (2024):

  • UMP (User Messaging Platform) required
  • Restrictions on implicit intents
  • Expanded SELinux restrictions
  • Enhanced security for WebViews

Key Areas to Test Across OS Versions

API Level Compatibility:

  • APIs available at each Android version
  • APIs marked deprecated (work but will be removed)
  • APIs removed in new versions

Test across your minimum supported API level and target API level.

Permissions Model:

  • Android 6+: Runtime permissions (ask at runtime, not install time)
  • Android 10+: Scoped storage
  • Android 11+: Microphone/camera indicators
  • Android 12+: Bluetooth runtime permission
  • Android 13+: Per-app language

Permissions handling varies significantly. Test every permission request on supported Android versions.

UI & Navigation:

  • Android 10+: Gesture navigation (replaces navigation buttons)
  • Screen sizes and aspect ratios vary by Android version
  • Material Design evolution (colors, typography)

Test UI across Android versions to catch layout/navigation issues.

Background Execution:

  • Android 8+: Background execution restrictions (can't run code in background indefinitely)
  • Android 10+: Location access restrictions
  • Android 12+: Foreground service restrictions

If your app uses background services, test extensively across versions.

Storage Access:

  • Pre-Android 10: Broad file access
  • Android 10+: Scoped storage (only access app-specific directories)
  • Android 11+: MANAGE_EXTERNAL_STORAGE permission deprecated

File access code works very differently across versions. Test thoroughly.

Network & Connectivity:

  • Cleartext traffic blocking (HTTPS required on Android 9+)
  • DNS over HTTPS support
  • Network security policies changed

Test network calls across versions.

Setting Up OS Version Testing

Android Emulator – Multiple OS Versions:

Create separate emulator instances for each Android version:

1. Open Android Studio

2. Tools → Device Manager

3. Create new device for each OS version (Android 10, 11, 12, 13, 14, 15)

4. Launch each separately as needed

Each emulator is isolated, letting you test version-specific behavior.

Physical Devices with Different OS Versions:

If using real devices, maintain devices with different OS versions:

  • One device on minimum API level (catch legacy issues)
  • One device on target API level (latest features)
  • One device on Android 12–13 (middle ground)

Firebase Test Lab – Multi-Version Testing:

Run automated tests across multiple OS versions in Firebase Test Lab:

1. Configure test matrix with OS versions

2. Run test suite

3. Tests execute on all OS versions automatically

4. Reports show failures by OS version

Cloud Device Labs:

Services like BrowserStack provide real devices across OS versions. Useful for manual testing.

Common OS Version Compatibility Issues

API Level Mismatches:

// WRONG: API 30+ only, will crash on Android 9
val response = MediaStore.getDocumentUri(...)

// CORRECT: Check API level first
val response = if (Build.VERSION.SDK_INT >= 30) {
    MediaStore.getDocumentUri(...)
} else {
    oldMethod()
}

Fix: Check API level before using new APIs. Use AndroidX compatibility libraries.

Permission Denials:

// WRONG: Assumes permission is granted
val location = LocationManager.requestLocationUpdates(...)

// CORRECT: Check permission first (Android 6+)
if (ContextCompat.checkSelfPermission(
    this, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED) {
    LocationManager.requestLocationUpdates(...)
} else {
    ActivityCompat.requestPermissions(...)
}

Fix: Always check permissions at runtime. Handle denials gracefully.

Scoped Storage Issues (Android 10+):

// WRONG: Direct file path access (won't work Android 10+)
val file = File("/storage/emulated/0/DCIM/photo.jpg")

// CORRECT: Use MediaStore or app-specific directory
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
val cursor = contentResolver.query(collection, ...)

Fix: Use MediaStore for shared files. Use app-specific directories for app data.

Deprecated Warnings:

When targeting new Android versions, you'll see deprecation warnings in Android Studio. Fix them before the deprecated API is removed.

Background Execution Failures (Android 8+):

// WRONG: Long-running background thread
thread {
    // This might be killed by Android
    while (true) {
        doWork()
    }
}

// CORRECT: Use foreground service or WorkManager
startForegroundService(Intent(this, MyService::class.java))
// OR
WorkManager.getInstance().enqueueUniquePeriodicWork(...)

Fix: Use appropriate background APIs for your use case (foreground services, WorkManager, etc.).

Testing Workflow for OS Versions

Step 1: Identify Supported OS Range

  • Minimum supported: Android 10 (or 11, 12 based on business decision)
  • Target: Latest Android version
  • Document this clearly

Step 2: Build Test Matrix

Android 10 (minimum)
Android 12
Android 14 (target)

Test at least 3 versions (min, mid, target). If supporting 6+ versions, test more versions.

Step 3: Set Up Testing Environment

Create emulators (or get devices) for each version. Keep list of devices.

Step 4: Automated Testing Across Versions

  • Write tests covering critical features
  • Run tests on all OS versions
  • Capture results by version

Step 5: Manual QA Testing

QA team tests critical features on real devices (especially minimum and target versions).

Step 6: Target Latest Android Version

Always target the latest Android version in your build.gradle:

android {
    compileSdk = 35  // Latest Android API
    targetSdkVersion = 35
    minSdkVersion = 24  // Minimum supported
}

Step 7: Monitor Post-Release

After release, use Play Console to monitor crashes by Android version. This reveals version-specific issues.

Handling Version-Specific Code

When OS versions have different behavior, use version checks:

Version Check Pattern:

when {
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
        // Android 13+ code
        useNewApi()
    }
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        // Android 12 code
        useCompatibleApi()
    }
    else -> {
        // Android < 12
        useLegacyApi()
    }
}

Using AndroidX Compatibility Libraries:

AndroidX provides compatibility wrappers for APIs across versions:

// Works across Android versions
ContextCompat.checkSelfPermission(context, permission)
ContextCompat.startActivity(context, intent, options)
NotificationCompat.Builder(context).setContentTitle("...").build()

Prefer AndroidX libraries over version checking when possible (cleaner code).

Testing Strategy by Feature Type

Files & Storage:

  • Test file write/read on Android 10+ (scoped storage changes)
  • Test directory access across versions
  • Test permission flows on Android 10+

Permissions:

  • Test runtime permission requests on Android 6+
  • Test permission denials
  • Test permission changes between OS versions

Background Tasks:

  • Test on Android 8+ (background restrictions)
  • Test foreground services
  • Test WorkManager

Notifications:

  • Test notification channels (Android 8+)
  • Test notification permissions (Android 13+)
  • Test notification appearance across versions

Location:

  • Test location permission (varies by version)
  • Test location access in background
  • Test approximate location (Android 12+)

Connectivity:

  • Test HTTPS enforcement (Android 9+)
  • Test network security policies
  • Test WiFi/cellular switching

Using Build Variants for Version-Specific Features

If your app needs drastically different implementations for different OS versions, use build variants:

flavorDimensions = ["osVersion"]
productFlavors {
    minSupported {
        targetSdk 34
    }
    latest {
        targetSdk 35
    }
}

This lets you build separate APKs optimized for different OS versions (advanced technique).

Testing Tools That Support Multiple Versions

Android Studio Emulator: Create multiple emulator instances for different OS versions.

Firebase Test Lab: Specify OS versions in test matrix, run tests automatically.

Espresso with Parameterized Tests: Run same tests across multiple OS versions.

@RunWith(Parameterized::class)
class OsVersionTests(private val osVersion: Int) {
    // Tests run on multiple OS versions
}

Device Simulation Platforms: Like DevicesChanger, let you define testing scenarios targeting specific OS versions, simplifying multi-version testing.

Conclusion

Testing across OS versions ensures your app works for all users, regardless of which Android version they're running. The key is smart sampling: test your minimum supported version (catch legacy issues), target version (leverage new features), and 1–2 middle versions.

Use emulators for rapid testing, cloud device labs for broader coverage, and real devices for high-impact features. Monitor crash data post-release to continuously improve version compatibility.

Teams that master OS version testing reduce version-specific bugs, support legacy Android versions confidently, and adopt new features quickly.

Related pages

How Device Changer fits

Structured device simulation and device profiles help teams run the workflows above with reproducible Android contexts—aligned with QA testing and mobile testing practice.

Try tool

Interface screenshots

FAQ

How does this relate to physical device labs?

Use simulation and profiles to scale coverage; validate critical builds on real hardware before release.

Where should automation sit in the pipeline?

Automate stable checks early (CI); keep exploratory and edge scenarios in dedicated QA passes.

More on the site