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
| Feature | Key Rule |
|---|---|
| Static shortcuts | Max 4; defined at compile time; cannot be removed by user unless app removes them |
| Dynamic shortcuts | Max 3–4; updated at runtime; cleared on app uninstall or data clear |
| Pinned shortcuts | Persist on home screen; user must remove manually |
reportShortcutUsed | Call on every use; improves ranking in Adaptive Shortcuts UI |
| PiP entry | Enter in onUserLeaveHint; check packageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE) |
| Hide controls in PiP | onPictureInPictureModeChanged → collapse controls to just the video |