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
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
- Predictive back gesture & animations — Android 14+ supports predictive back — the system shows a preview of the destination (home screen, previous activity) as the user swipes back. Apps must opt in via android:enableOnBackInvokedCallback='true' in the manifest. Legacy onBackPressed() is deprecated — migrate to OnBackPressedCallback for proper handling. Custom back animations use Progress-based APIs.
- Per-app language preferences (LocaleManager) — Android 13+ lets users set a different language per app via system settings. Use LocaleManager.setApplicationLocales() or the AppCompatDelegate.setApplicationLocales() compat API. Declare supported locales in res/xml/locales_config.xml and reference it in the manifest with android:localeConfig. This replaces hacky custom locale-switching implementations.
- Photo picker (PickVisualMedia) — ActivityResultContracts.PickVisualMedia() provides a system-level media picker without requiring READ_EXTERNAL_STORAGE. Available on Android 13+ natively and backported to Android 11+ via Google Play Services. Supports filtering by ImageOnly, VideoOnly, or ImageAndVideo. Use PickMultipleVisualMedia(maxItems) for multi-select.
- Foreground service types (Android 14 requirement) — Android 14 requires all foreground services to declare a type in the manifest: camera, connectedDevice, dataSync, health, location, mediaPlayback, mediaProjection, microphone, phoneCall, remoteMessaging, shortService, specialUse, systemExempted. Missing type declarations crash with MissingForegroundServiceTypeException. Each type requires corresponding permissions.
- Partial media access (READ_MEDIA_VISUAL_USER_SELECTED) — Android 14 introduces partial photo/video access — users can grant access to selected media items instead of all photos. Apps must handle the new READ_MEDIA_VISUAL_USER_SELECTED permission and re-request access when they need more items. The photo picker is the recommended alternative to avoid permission complexity entirely.
- Edge-to-edge enforcement (Android 15) — Android 15 enforces edge-to-edge display for apps targeting SDK 35+. The system draws behind status bar and navigation bar by default. Apps must use WindowInsets to handle padding — Modifier.windowInsetsPadding() in Compose or ViewCompat.setOnApplyWindowInsetsListener() in Views. Status bar and nav bar backgrounds become transparent — no opt-out.
- targetSdk migration checklist pattern — When bumping targetSdkVersion: (1) Read the behavior changes doc for ALL versions between current and target. (2) Test with StrictMode enabled. (3) Check for deprecated API usage. (4) Update foreground service types (14+). (5) Handle new permissions model changes. (6) Test edge-to-edge layout (15+). (7) Verify back navigation behavior. Always bump one version at a time.
- Privacy Sandbox & advertising changes — Privacy Sandbox replaces the Advertising ID with privacy-preserving APIs: Topics API (interest-based ads without tracking), Attribution Reporting (conversion measurement without cross-app tracking), and Protected Audiences (remarketing without user profiles). GAID will be deprecated. Apps using ads SDKs need to plan migration.
- Grammatical Inflection API (Android 14) — Android 14 adds GrammaticalInflectionManager for gendered languages. Apps can set the user's grammatical gender so the system inflects UI strings correctly (e.g., masculine/feminine forms in French, German, etc.). Call setRequestedApplicationGrammaticalGender() with GENDER_MASCULINE, GENDER_FEMININE, or GENDER_NEUTRAL.
- Screenshot detection API (Android 14) — Android 14 provides Activity.ScreenCaptureCallback for detecting when the user takes a screenshot. Register via registerScreenCaptureCallback(). Useful for financial/banking apps that want to warn users about sensitive content being captured. Replaces unreliable ContentObserver-based hacks for detecting screenshots.
- Credential Manager API — Android 14 introduces Credential Manager as the unified API for sign-in. It consolidates passkeys, passwords, and federated identity (Google Sign-In) into a single flow. Replaces the legacy Smart Lock for Passwords API. Call CredentialManager.getCredential() with a GetCredentialRequest containing the supported credential types.
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. OnBackPressedCallback(true) — creates a callback that is initially enabled; when enabled, it intercepts back presses before the system handles them
- 2. addCallback(viewLifecycleOwner, backCallback) — ties the callback lifecycle to the Fragment's view; automatically removed when view is destroyed, preventing leaks
- 3. isEnabled = false; onBackPressed() — disables the callback and re-dispatches the back press so the system (or next callback) handles it normally
- 4. registerForActivityResult(PickVisualMedia()) — sets up the photo picker contract; the lambda receives a nullable Uri of the selected media
- 5. PickVisualMediaRequest(ImageOnly) — configures the picker to only show images; alternatives are VideoOnly and ImageAndVideo
- 6. ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC) — starts foreground with explicit type; the type must match the manifest declaration
- 7. enableEdgeToEdge() — extends the app's drawing area behind system bars; must handle insets to avoid content overlap with status/navigation bars
- 8. Modifier.padding(innerPadding) — applies the insets-aware padding provided by Scaffold; ensures content does not render behind system bars
- 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. 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Android 14 Behavior Changes (developer.android.com)
- Android 15 Behavior Changes (developer.android.com)
- Predictive Back Design Guide (developer.android.com)
- Photo Picker (developer.android.com)
- Edge-to-Edge Display (developer.android.com)