Lesson 78 of 83 advanced

Android 14/15 Platform Features & Migration

Stay current with the latest platform requirements and APIs that interviewers expect you to know

Open interactive version (quiz + challenge)

Real-world analogy

Android platform updates are like building code revisions for a city. Each year, the city council (Google) passes new regulations. Some are optional at first (targetSdk not bumped yet), but eventually become mandatory. Predictive back is like adding exit signs with preview windows — you can peek through the door before leaving. Edge-to-edge is like removing all window frames so you get a floor-to-ceiling glass wall.

What is it?

Android 14 (API 34) and Android 15 (API 35) introduce significant platform changes that affect how apps handle back navigation, media access, foreground services, display rendering, and privacy. These updates follow Google's pattern of introducing features as opt-in first, then making them mandatory when apps target the new SDK. Senior engineers must understand these changes for migration planning and interviews.

Real-world relevance

A banking app targeting SDK 34 faced three major migration issues: (1) All 12 foreground services needed type declarations — the background sync service required the dataSync type with DATA_SYNC permission. (2) The custom back-handling in the transaction flow used onBackPressed(), which broke predictive back animations — migration to OnBackPressedCallback took 3 sprints because of complex navigation state. (3) The photo ID upload feature used READ_EXTERNAL_STORAGE which no longer grants access on Android 14 — migrating to the photo picker simplified the code from 200 lines to 15 lines.

Key points

Code example

// 1. Predictive Back — AndroidManifest.xml
// <application android:enableOnBackInvokedCallback="true" ...>

// Modern back handling with OnBackPressedCallback
class CheckoutFragment : Fragment() {

    private val backCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (hasUnsavedChanges()) {
                showDiscardDialog()
            } else {
                isEnabled = false
                requireActivity().onBackPressedDispatcher.onBackPressed()
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        requireActivity().onBackPressedDispatcher
            .addCallback(viewLifecycleOwner, backCallback)
    }
}

// 2. Photo Picker — no permissions needed!
class ProfileFragment : Fragment() {

    private val pickMedia = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia()
    ) { uri ->
        uri?.let { uploadProfilePhoto(it) }
    }

    private fun selectPhoto() {
        pickMedia.launch(
            PickVisualMediaRequest(
                ActivityResultContracts.PickVisualMedia.ImageOnly
            )
        )
    }
}

// 3. Foreground Service Type — AndroidManifest.xml
// <service
//     android:name=".sync.DataSyncService"
//     android:foregroundServiceType="dataSync"
//     android:exported="false" />

// Starting with type in code (Android 14+)
class DataSyncService : Service() {

    override fun onStartCommand(
        intent: Intent?, flags: Int, startId: Int
    ): Int {
        val notification = createNotification()
        ServiceCompat.startForeground(
            this,
            NOTIFICATION_ID,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
        )
        performSync()
        return START_NOT_STICKY
    }
}

// 4. Edge-to-Edge (Android 15 mandatory)
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge() // From androidx.activity

        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                contentWindowInsets = ScaffoldDefaults
                    .contentWindowInsets
            ) { innerPadding ->
                MainScreen(
                    modifier = Modifier.padding(innerPadding)
                )
            }
        }
    }
}

// 5. Per-App Language Preferences
// res/xml/locales_config.xml:
// <locale-config xmlns:android="...">
//     <locale android:name="en" />
//     <locale android:name="bn" />
//     <locale android:name="hi" />
//     <locale android:name="es" />
// </locale-config>

fun changeLanguage(context: Context, languageTag: String) {
    val localeList = LocaleListCompat.forLanguageTags(languageTag)
    AppCompatDelegate.setApplicationLocales(localeList)
}

// 6. Screenshot Detection (Android 14+)
class SensitiveActivity : AppCompatActivity() {

    private val screenshotCallback = Activity.ScreenCaptureCallback {
        showWarningDialog("Screenshot detected on sensitive screen")
    }

    override fun onStart() {
        super.onStart()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            registerScreenCaptureCallback(mainExecutor, screenshotCallback)
        }
    }

    override fun onStop() {
        super.onStop()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            unregisterScreenCaptureCallback(screenshotCallback)
        }
    }
}

Line-by-line walkthrough

  1. 1. OnBackPressedCallback(true) — creates a callback that is initially enabled; when enabled, it intercepts back presses before the system handles them
  2. 2. addCallback(viewLifecycleOwner, backCallback) — ties the callback lifecycle to the Fragment's view; automatically removed when view is destroyed, preventing leaks
  3. 3. isEnabled = false; onBackPressed() — disables the callback and re-dispatches the back press so the system (or next callback) handles it normally
  4. 4. registerForActivityResult(PickVisualMedia()) — sets up the photo picker contract; the lambda receives a nullable Uri of the selected media
  5. 5. PickVisualMediaRequest(ImageOnly) — configures the picker to only show images; alternatives are VideoOnly and ImageAndVideo
  6. 6. ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC) — starts foreground with explicit type; the type must match the manifest declaration
  7. 7. enableEdgeToEdge() — extends the app's drawing area behind system bars; must handle insets to avoid content overlap with status/navigation bars
  8. 8. Modifier.padding(innerPadding) — applies the insets-aware padding provided by Scaffold; ensures content does not render behind system bars
  9. 9. AppCompatDelegate.setApplicationLocales(localeList) — changes the app's locale using the compat API; works on Android 13+ natively and uses AppCompat on older versions
  10. 10. registerScreenCaptureCallback(mainExecutor, callback) — registers for screenshot notifications on the main thread; must be balanced with unregister in onStop()

Spot the bug

class PaymentActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_payment)
    }

    // Legacy back handling — breaks predictive back
    override fun onBackPressed() {
        if (hasUnsavedTransaction()) {
            showConfirmDialog()
        } else {
            super.onBackPressed()
        }
    }
}

// Service without foregroundServiceType
// <service android:name=".LocationService" />

class LocationService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, id: Int): Int {
        startForeground(1, createNotification())
        startLocationUpdates()
        return START_STICKY
    }
}
Need a hint?
Two issues: deprecated back navigation approach and missing foreground service type for Android 14+
Show answer
Bug 1: onBackPressed() is deprecated and prevents predictive back animations. Fix: Remove the override, add android:enableOnBackInvokedCallback='true' to manifest, and register an OnBackPressedCallback in onCreate with the unsaved transaction logic. Bug 2: LocationService has no foregroundServiceType in the manifest or startForeground() call. On Android 14+, this crashes with MissingForegroundServiceTypeException. Fix: Add android:foregroundServiceType='location' to the <service> tag, add ACCESS_FINE_LOCATION permission, and use ServiceCompat.startForeground(this, 1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION).

Explain like I'm 5

Imagine your phone is a house that gets renovated every year. Android 14 said 'every room that runs while you sleep (foreground services) must now have a label on the door saying what it does.' Android 15 said 'we are removing all the window borders so you can see outside from floor to ceiling (edge-to-edge), and you cannot put the borders back.' The photo picker is like a librarian who brings you only the photos you ask for, instead of giving you the keys to the entire photo library.

Fun fact

Android 14's codename was 'Upside Down Cake' — the first Android version name since Android 10 (Q) that Google publicly used as a codename. The internal dessert naming tradition never stopped; Google just stopped using them publicly for marketing. The foreground service type requirement in Android 14 was one of the most impactful changes — it broke thousands of apps on the Play Store that had to rush updates before the targetSdk deadline.

Hands-on challenge

Build an Activity that demonstrates four Android 14/15 features: (1) Register an OnBackPressedCallback that shows a confirmation dialog when there are unsaved changes. (2) Implement photo selection using PickVisualMedia with both single and multi-select modes. (3) Start a foreground service with the correct foregroundServiceType for location tracking. (4) Apply edge-to-edge display using enableEdgeToEdge() and handle WindowInsets properly so content is not hidden behind system bars. Add proper SDK version checks throughout.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery