androidengineers.Book a session

Android Platform Services

Shortcuts & Picture-in-Picture

article15 minMedium

App shortcuts give users fast access to specific actions from the launcher icon. Picture-in-Picture lets video or video-call content continue playing in a small overlay while users multitask.

App Shortcuts

Static Shortcuts (XML — compile time)

<!-- res/xml/shortcuts.xml -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
    <shortcut
        android:shortcutId="new_post"
        android:enabled="true"
        android:icon="@drawable/ic_create"
        android:shortcutShortLabel="@string/shortcut_new_post_short"
        android:shortcutLongLabel="@string/shortcut_new_post_long">
        <intent
            android:action="android.intent.action.VIEW"
            android:targetPackage="com.myapp"
            android:targetClass="com.myapp.MainActivity">
            <extra android:name="destination" android:value="new_post" />
        </intent>
        <categories android:name="android.shortcut.conversation" />
    </shortcut>
</shortcuts>
<!-- Declare in AndroidManifest.xml launcher activity -->
<meta-data
    android:name="android.app.shortcuts"
    android:resource="@xml/shortcuts" />

Dynamic Shortcuts (Runtime)

fun updateDynamicShortcuts(context: Context, recentConversations: List<Conversation>) {
    val manager = context.getSystemService(ShortcutManager::class.java)

    val shortcuts = recentConversations.take(3).map { conv ->
        val intent = Intent(context, MainActivity::class.java).apply {
            action = Intent.ACTION_VIEW
            putExtra("destination", "conversation")
            putExtra("conversation_id", conv.id)
        }

        ShortcutInfo.Builder(context, "conv_${conv.id}")
            .setShortLabel(conv.name)
            .setLongLabel("Chat with ${conv.name}")
            .setIcon(Icon.createWithBitmap(conv.avatar))
            .setIntent(intent)
            .build()
    }

    manager.dynamicShortcuts = shortcuts
}

// Increment usage rank so most-used shortcuts surface first
fun reportShortcutUsed(context: Context, conversationId: String) {
    val manager = context.getSystemService(ShortcutManager::class.java)
    manager.reportShortcutUsed("conv_$conversationId")
}

// Pinned shortcut (user places on home screen)
fun requestPinnedShortcut(context: Context, conversationId: String) {
    val manager = context.getSystemService(ShortcutManager::class.java)
    if (manager.isRequestPinShortcutSupported) {
        val shortcut = ShortcutInfo.Builder(context, "conv_$conversationId")
            .setShortLabel("Quick chat")
            .setIntent(Intent(/* ... */))
            .build()
        manager.requestPinShortcut(shortcut, null)
    }
}

Picture-in-Picture

Enter PiP (e.g., when user presses Home during video)

class VideoActivity : ComponentActivity() {

    private fun buildPipParams(): PictureInPictureParams {
        val aspectRatio = Rational(16, 9)

        val remoteActions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            listOf(
                buildPipAction(
                    context = this,
                    iconRes = R.drawable.ic_pause,
                    title = "Pause",
                    requestCode = 1001,
                    action = "com.myapp.action.PAUSE"
                )
            )
        } else emptyList()

        return PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            .setActions(remoteActions)
            .build()
    }

    override fun onUserLeaveHint() {
        if (isVideoPlaying()) {
            enterPictureInPictureMode(buildPipParams())
        }
    }

    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        // Hide/show controls based on PiP state
        viewBinding.controls.isVisible = !isInPictureInPictureMode
    }
}

PiP Compose Integration

@Composable
fun VideoScreen(viewModel: VideoViewModel = hiltViewModel()) {
    val context = LocalContext.current
    val activity = context as ComponentActivity

    val isInPip = remember { mutableStateOf(false) }

    DisposableEffect(activity) {
        val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
            isInPip.value = info.isInPictureInPictureMode
        }
        activity.addOnPictureInPictureModeChangedListener(observer)
        onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
    }

    Box {
        VideoPlayer(modifier = Modifier.fillMaxSize())

        if (!isInPip.value) {
            VideoControls(
                onEnterPip = {
                    activity.enterPictureInPictureMode(
                        PictureInPictureParams.Builder()
                            .setAspectRatio(Rational(16, 9))
                            .build()
                    )
                }
            )
        }
    }
}

Handle PiP Actions (Broadcast)

class PipActionReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            "com.myapp.action.PAUSE" -> EventBus.post(PauseVideoEvent())
            "com.myapp.action.PLAY"  -> EventBus.post(PlayVideoEvent())
        }
    }
}

Key Takeaways

FeatureKey Rule
Static shortcutsMax 4; defined at compile time; cannot be removed by user unless app removes them
Dynamic shortcutsMax 3–4; updated at runtime; cleared on app uninstall or data clear
Pinned shortcutsPersist on home screen; user must remove manually
reportShortcutUsedCall on every use; improves ranking in Adaptive Shortcuts UI
PiP entryEnter in onUserLeaveHint; check packageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)
Hide controls in PiPonPictureInPictureModeChanged → collapse controls to just the video

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Shortcuts & Picture-in-Picture | Android System Design | Android Engineers