Picture-in-picture feels effortless to the viewer only when the app treats it as part of playback state, not as a miniature second Activity. Android owns the floating window; your Activity supplies video, hints, and a small action surface while remaining responsible for media state and lifecycle.
When PiP is appropriate
Long-form or live video the user intentionally started and wants to keep watching.
Video calls or navigation experiences where continuity is central and policy permits it.
Not advertisements, surprise autoplay, static content, or a trick to keep arbitrary background work alive.
Not a replacement for media sessions, audio-focus handling, foreground-service rules, or playback notifications.
Not guaranteed on every form factor; feature detection and product fallback remain necessary.
Define one eligibility predicate
Platform and device support are present.
The current media is playing or in an intentionally eligible buffering state.
The user initiated playback and has not explicitly closed/minimized it.
Entitlement, DRM, privacy, and product policy allow floating playback.
No error, ad boundary, sensitive screen, or terminal state forbids entry.
1. Declare the activity capability
<activity
android:name=".PlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:exported="false"
android:supportsPictureInPicture="true" />The manifest enables, but does not enter, PiP
supportsPictureInPicturetells Android this activity can be placed into PiP.Handling the listed configuration changes avoids unnecessary activity recreation during the PiP transition; the activity must still update resources/layout correctly.
exported=falseis suitable when no external component needs to launch this player directly; product navigation may require a different reviewed value.PiP does not require a dangerous runtime permission.
The application must still call/configure the PiP APIs based on meaningful playback state.
2. Detect platform support
private fun supportsPictureInPicture(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)API level and device feature are separate checks
The class/method surface begins at API 26.
The package-manager feature reports whether this device implementation exposes PiP.
The user or administrator may still disable PiP for the app.
Hide or disable manual PiP affordances when unsupported, while leaving ordinary playback usable.
Do not cache capability across device-policy or app-setting changes without a refresh path.
3. Build PictureInPictureParams from player state
private fun updatePipParams(playerView: View, isPlaying: Boolean) {
if (!supportsPictureInPicture()) return
val rect = Rect()
playerView.getGlobalVisibleRect(rect)
val builder = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.setSourceRectHint(rect)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder
.setAutoEnterEnabled(isPlaying)
.setSeamlessResizeEnabled(true)
}
setPictureInPictureParams(builder.build())
}Parameters are hints to the system
Rational(16, 9)describes the current video aspect, not necessarily the device screen.Android constrains supported aspect ratios and may adjust the requested shape.
sourceRectHinthelps the system animate from the visible player bounds.Auto-entry should be enabled only while content is actively eligible, then disabled when paused/ended/error.
Seamless resize works best for video surfaces that can resize without visibly relayouting surrounding UI.
Call updates when playback/video bounds/aspect changes; avoid rebuilding on every frame.
4. Enter PiP manually where needed
private fun enterPipIfEligible(): Boolean {
if (!supportsPictureInPicture() || !player.isPlaying) return false
updatePipParams(playerView, isPlaying = true)
return enterPictureInPictureMode(pictureInPictureParams)
}The return value is evidence, not a promise
The method requests entry and returns whether Android accepted the request.
Only enter for user-initiated, currently playing content under the app’s product rules.
Do not assume a failed request means playback must stop; retain the full-screen experience.
Catch lifecycle/state races through tests rather than wrapping the API in a broad exception handler.
On Android 12+, auto-entry generally gives a smoother Home/swipe-up transition than calling entry from
onUserLeaveHint().
5. Support pre-Android 12 navigation
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O until Build.VERSION_CODES.S) {
enterPipIfEligible()
}
}Keep the fallback version-scoped
onUserLeaveHint()is a hint that the user is leaving; it is not called for every background transition.The range limits this manual behavior to API 26–30 when modern auto-entry is unavailable.
Do not enter PiP after pressing a deliberate in-app back/close control unless that is the clearly communicated interaction.
Do not trigger PiP for dialogs, permission flows, screen lock, errors, or nonplaying content.
Test gesture navigation, three-button navigation, Home, Recents, Back, and screen-off separately.
6. Adapt UI when PiP mode changes
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration,
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
controlsContainer.isVisible = !isInPictureInPictureMode
titleBar.isVisible = !isInPictureInPictureMode
}PiP should show content, not a squeezed page
Hide navigation, metadata, and custom controls that are unusable at tiny size.
Android supplies system PiP controls and app-provided remote actions.
Keep the video surface laid out edge-to-edge within the PiP content bounds.
Restore full UI when expanding out of PiP.
The callback overload shown is available on current APIs while the Activity remains guarded by the API-26 feature boundary.
Do not release the player merely because the Activity entered PiP.
7. Keep Media3 player lifetime separate from view chrome
override fun onStart() {
super.onStart()
playerView.player = player
}
override fun onStop() {
super.onStop()
if (!isInPictureInPictureMode) {
player.pause()
playerView.player = null
}
}
override fun onDestroy() {
player.release()
super.onDestroy()
}Adapt ownership to the real playback architecture
This sketch assumes the Activity owns the player; a playback service/session changes release and binding responsibilities.
PiP playback may continue through
onStop()variations across navigation/device states, so test the exact lifecycle rather than copying one callback blindly.A MediaSession improves system/media control integration and is essential in many real playback apps.
Handle audio focus, noisy audio routes, calls, headset removal, and foreground-service policy independently.
Release every player exactly once when its owner is permanently destroyed.
Persist playback position/media identity across recreation or process death as the product requires.
8. Add a small set of remote actions
private fun playbackAction(icon: Icon, title: String, intent: PendingIntent): RemoteAction =
RemoteAction(icon, title, title, intent).apply {
isEnabled = true
}
private fun actionIntent(action: String, requestCode: Int): PendingIntent {
val intent = Intent(this, PipActionReceiver::class.java)
.setAction(action)
.setPackage(packageName)
return PendingIntent.getBroadcast(
this,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}Actions must be explicit and current
Remote actions are displayed by the system, subject to platform limits and UI decisions.
Use unique request codes/actions so play and pause intents do not overwrite one another.
FLAG_IMMUTABLEprotects intent fields from mutation by recipients.Explicitly package or component-scope broadcasts and validate actions in the receiver.
Update the action list when playback changes instead of showing a stale play/pause command.
Use concise localized titles and accessible icons; avoid duplicating system-provided controls unnecessarily.
Set actions on the current parameter object
val params = PictureInPictureParams.Builder()
.setAspectRatio(currentAspectRatio)
.setActions(listOf(playOrPauseAction))
.build()
setPictureInPictureParams(params)Rebuild the complete intended state
A newly built params object should retain every relevant aspect/source/action/auto-entry setting.
Do not accidentally drop auto-entry while updating actions.
Keep action computation in one state reducer/helper to avoid competing partial builders.
Use player callbacks to trigger updates at meaningful state transitions.
Remove actions that no longer apply after end/error/restricted content.
Android 13+ expanded aspect guidance
Newer APIs add an expanded aspect-ratio hint for content that benefits from a different shape when the user expands PiP. Guard newer setters by API level and treat them as hints. A normal PiP experience must remain correct on API 26 and on devices that ignore or constrain the hint.
Compose UI still uses the platform Activity API
Jetpack Compose can observe PiP state and hide composables, but the platform transition remains an Activity responsibility. Keep player state in an appropriate owner, obtain/track the video bounds safely, update params from stable effects, and avoid repeatedly calling setters during recomposition.
Check whether the app is currently in PiP
private val inPip: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
isInPictureInPictureModeCurrent state drives cleanup and UI
The property is safe on pre-26 devices because the platform method is version-guarded.
Use it for lifecycle/UI decisions, not as the sole proof that playback should continue.
Player eligibility also depends on media state, user intent, permissions/DRM policy, and app ownership.
A process can still be killed; persist enough state to recover gracefully.
Build and install a test variant
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebug
adb devices -l
./gradlew :app:installDebugBUILD SUCCESSFUL
<authorized device listed>
Installed on 1 device.Build success does not exercise PiP
The Wrapper selects the committed Gradle distribution.
Unit tests and lint catch logic/static issues but cannot simulate all system PiP behavior.
Confirm the target device is authorized before installation.
Use an emulator/device image that supports the API/feature under test.
Keep debug and production media/network/DRM behavior distinct and test the release-like path too.
Device test matrix
API 26–30: legacy Home navigation enters only while eligible.
API 31+: auto-entry animation, gesture navigation, and source-rect transition.
Phone, tablet, foldable/large screen, and TV/form-factor behavior relevant to the product.
Portrait/landscape video, unusual aspect ratios, rotation, resize, split-screen, and configuration changes.
Play, pause, seek, buffering, ended, error, next item, live stream, and ad boundaries.
Expand, close, dismiss, tap actions, return to app, Back/Home/Recents, screen lock, and process recreation.
PiP disabled in app settings or unavailable by device/policy.
Audio focus loss, calls, Bluetooth/headset changes, network loss, and media-session controls.
Accessibility: TalkBack labels, focus, captions/subtitles, contrast, and action clarity.
Inspect activity and logs while testing
adb shell dumpsys activity activities | rg -i "picture-in-picture|mPip|PlayerActivity"
adb logcat -d | rg "PlayerActivity|Media3|ExoPlayer|PictureInPicture"Inspect device-specific activity state and player diagnostics.Use system evidence alongside visual observation
dumpsys output changes across Android versions; treat field names as diagnostic, not an API contract.
Log player and PiP state transitions without media URLs, tokens, personal content, or DRM secrets.
Correlate timestamps for user navigation, playback state, and callbacks.
Clear logs only on an appropriate test device when losing history is acceptable.
Automated UI tests should assert app state while manual/device-lab tests validate system animation and controls.
Record the lifecycle sequence
Capture playback state before the navigation gesture.
Record the PiP parameter update and entry request/auto-entry eligibility.
Record mode-change, start/stop, focus, and player callbacks in timestamp order.
Confirm UI chrome, player attachment, media session, and audio behavior at each transition.
Repeat the same sequence for expansion, dismissal, error, screen lock, and process recreation.
Common PiP failures
Nothing happens: verify API 26+, device feature, manifest flag, current activity, playback eligibility, and user/app PiP setting.
PiP opens for dialogs or permission screens: remove entry from
onPause()and scope auto/legacy triggers.A black frame appears: keep the player/surface attached and verify decoder/surface lifecycle during resize.
Controls fill the tiny window: hide app chrome in the PiP callback and use limited remote actions.
The wrong aspect ratio appears: update params from actual video dimensions and account for rotation/pixel aspect.
Transition jumps: provide a current visible source rectangle and Android 12 auto-entry.
Playback stops in PiP: inspect Activity/service/player ownership and lifecycle callbacks.
Audio continues after dismissal: observe stop/dismiss/player/session state and define close behavior explicitly.
Action does nothing: inspect PendingIntent uniqueness, receiver registration/export, package scoping, and state updates.
Works on one device only: compare API, OEM PiP support/settings, navigation mode, form factor, and media implementation.
Security, privacy, and product constraints
Respect DRM/content-provider rules that may prohibit or restrict PiP.
Use secure-window policy deliberately; protected video/screenshot behavior varies with requirements and platform support.
Do not expose sensitive account/video metadata in PiP actions, icons, notifications, or logs.
Make broadcast/PendingIntent targets explicit and immutable where possible.
Do not start PiP without clear prior user playback intent.
Stop/restrict playback when authentication, entitlement, or session validity changes.
PiP does not grant background execution privileges or exempt the app from foreground-service/media policies.
Completion checklist
Activity capability and configuration handling are declared intentionally.
API/device/user-setting support is checked with a normal playback fallback.
Params track aspect, visible bounds, eligibility, auto-entry, resize, and actions.
Android 12+ uses auto-entry; legacy entry is scoped to API 26–30 and user navigation.
PiP UI hides unusable chrome and restores it on expansion.
Player/session/service lifecycle is tested independently of view visibility.
PendingIntents/actions are explicit, immutable, localized, state-aware, and minimal.
All playback, navigation, lifecycle, form-factor, accessibility, and failure cases in the device matrix pass.
Logs/artifacts reveal no sensitive data and release behavior is verified.
Official Android references
Picture-in-picture developer guide documents manifest setup, params, auto-entry, callbacks, actions, and testing.
PictureInPictureParams.Builder defines aspect, source, action, auto-entry, resize, and expanded-aspect APIs by level.
Media3 PiP guidance connects player behavior and modern PiP handling.
Multi-window support explains lifecycle/configuration behavior across window modes.
Media sessions covers system playback control architecture for real media apps.
Comments and corrections