Các nút điều khiển nội dung nghe nhìn trong Android nằm gần phần Cài đặt nhanh. Các phiên từ nhiều ứng dụng được sắp xếp trong một băng chuyền có thể vuốt. Băng chuyền liệt kê các phiên theo thứ tự sau:
- Các luồng phát cục bộ trên điện thoại
- Các luồng từ xa, chẳng hạn như các luồng được phát hiện trên thiết bị bên ngoài hoặc các phiên truyền
- Các phiên có thể tiếp tục trước đó, theo thứ tự phát gần đây nhất
Kể từ Android 13 (cấp độ API 33), để đảm bảo người dùng có thể truy cập vào một bộ điều khiển nội dung nghe nhìn phong phú cho các ứng dụng phát nội dung nghe nhìn, các nút hành động trên các nút điều khiển nội dung nghe nhìn được lấy từ trạng thái Player.
Bằng cách này, bạn có thể trình bày một bộ điều khiển nội dung nghe nhìn nhất quán và trải nghiệm điều khiển nội dung nghe nhìn tinh tế hơn trên các thiết bị.
Hình 1 cho thấy ví dụ về cách hiển thị trên điện thoại và thiết bị máy tính bảng.
Hệ thống sẽ hiển thị tối đa 5 nút hành động dựa trên trạng thái Player như mô tả trong bảng sau. Ở chế độ thu gọn, chỉ 3 vị trí thao tác đầu tiên sẽ xuất hiện. Điều này phù hợp với cách các nút điều khiển nội dung nghe nhìn được kết xuất trên các nền tảng Android khác, chẳng hạn như Auto, Trợ lý và Wear OS.
Nút trình đơn tuỳ chỉnh được đặt theo thứ tự thêm vào lựa chọn ưu tiên về nút nội dung nghe nhìn.
Tuỳ chỉnh các nút lệnh
Để tuỳ chỉnh các nút điều khiển nội dung nghe nhìn của hệ thống bằng Jetpack Media3, bạn có thể đặt lựa chọn ưu tiên về nút nội dung nghe nhìn của phiên và các lệnh có sẵn của bộ điều khiển cho phù hợp:
Tạo
MediaSessionvà xác định lựa chọn ưu tiên về nút nội dung nghe nhìn cho các nút lệnh tuỳ chỉnh.Trong
MediaSession.Callback.onConnect(), hãy uỷ quyền cho bộ điều khiển bằng cách xác định các lệnh có sẵn của chúng, bao gồm cả các lệnh tuỳ chỉnh commands, trongConnectionResult.Trong
MediaSession.Callback.onCustomCommand(), hãy phản hồi lệnh tuỳ chỉnh mà người dùng chọn.
Kotlin
class PlaybackService : MediaSessionService() { private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) private var mediaSession: MediaSession? = null override fun onCreate() { super.onCreate() val favoriteButton = CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) .build() } private inner class MyCallback : MediaSession.Callback { override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { // Set available player and session commands. return AcceptedResultBuilder(session) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandFavorites) .build() ) .build() } override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture{ if (customCommand.customAction == ACTION_FAVORITES) { // Do custom logic here saveToFavorites(session.player.currentMediaItem) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } return super.onCustomCommand(session, controller, customCommand, args) } } }
Java
public class PlaybackService extends MediaSessionService { private static final SessionCommand CUSTOM_COMMAND_FAVORITES = new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY); @Nullable private MediaSession mediaSession; public void onCreate() { super.onCreate(); CommandButton favoriteButton = new CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .setSessionCommand(CUSTOM_COMMAND_FAVORITES) .build(); Player player = new ExoPlayer.Builder(this).build(); // Build the session with a custom layout. mediaSession = new MediaSession.Builder(this, player) .setCallback(new MyCallback()) .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) .build(); } private static class MyCallback implements MediaSession.Callback { @Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { // Set available player and session commands. return new AcceptedResultBuilder(session) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(CUSTOM_COMMAND_FAVORITES) .build()) .build(); } public ListenableFutureonCustomCommand( MediaSession session, MediaSession.ControllerInfo controller, SessionCommand customCommand, Bundle args) { if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } return MediaSession.Callback.super.onCustomCommand( session, controller, customCommand, args); } } }
Để tìm hiểu thêm về cách định cấu hình MediaSession để các ứng dụng như hệ thống có thể kết nối với ứng dụng đa phương tiện của bạn, hãy xem Cấp quyền kiểm soát cho các ứng dụng khác.
Với Jetpack Media3, khi bạn triển khai MediaSession, PlaybackState sẽ tự động được cập nhật theo trình phát nội dung nghe nhìn. Tương tự, khi bạn
triển khai MediaSessionService, thư viện sẽ tự động xuất bản
MediaStyle thông báo
cho bạn và luôn cập nhật thông báo đó.
Phản hồi các nút hành động
Khi người dùng nhấn vào một nút hành động trong các nút điều khiển nội dung nghe nhìn của hệ thống, MediaController của hệ thống sẽ gửi lệnh phát đến MediaSession. Sau đó, MediaSession sẽ uỷ quyền các lệnh đó cho trình phát. Các lệnh
được xác định trong giao diện Player
của Media3 sẽ tự động được phiên nội dung nghe nhìn
xử lý.
Hãy tham khảo phần Thêm lệnh tuỳ chỉnh để được hướng dẫn cách phản hồi lệnh tuỳ chỉnh.
Hỗ trợ tiếp tục phát nội dung nghe nhìn
Tính năng tiếp tục phát nội dung nghe nhìn cho phép người dùng khởi động lại các phiên trước đó từ băng chuyền mà không cần phải khởi động ứng dụng. Khi quá trình phát bắt đầu, người dùng sẽ tương tác với các nút điều khiển nội dung nghe nhìn theo cách thông thường.
Bạn có thể bật và tắt tính năng tiếp tục phát bằng ứng dụng Cài đặt, trong phần Âm thanh > Nội dung nghe nhìn. Người dùng cũng có thể truy cập vào phần Cài đặt bằng cách nhấn vào biểu tượng bánh răng xuất hiện sau khi vuốt trên băng chuyền mở rộng.
Media3 cung cấp các API giúp bạn dễ dàng hỗ trợ tính năng tiếp tục phát nội dung nghe nhìn. Hãy xem tài liệu Tiếp tục phát bằng Media3 để được hướng dẫn cách triển khai tính năng này.
Sử dụng các API nội dung nghe nhìn cũ
Phần này giải thích cách tích hợp với các nút điều khiển nội dung nghe nhìn của hệ thống bằng các API MediaCompat cũ.
Hệ thống truy xuất thông tin sau đây từ MediaMetadata của MediaSession và hiển thị thông tin đó khi có:
METADATA_KEY_ALBUM_ART_URIMETADATA_KEY_TITLEMETADATA_KEY_DISPLAY_TITLEMETADATA_KEY_ARTISTMETADATA_KEY_DURATION(Nếu bạn không đặt thời lượng, thanh tua sẽ không hiển thị tiến trình)
Để đảm bảo bạn có thông báo hợp lệ và chính xác về nút điều khiển nội dung nghe nhìn, hãy đặt giá trị của siêu dữ liệu METADATA_KEY_TITLE hoặc METADATA_KEY_DISPLAY_TITLE thành tiêu đề của nội dung nghe nhìn đang phát.
Trình phát nội dung nghe nhìn cho biết thời gian đã trôi qua của nội dung nghe nhìn đang phát, cùng với thanh tua được ánh xạ đến PlaybackState MediaSession.
Trình phát nội dung nghe nhìn cho biết tiến trình của nội dung nghe nhìn đang phát, cùng với thanh tua được ánh xạ đến PlaybackState MediaSession. Thanh tua cho phép người dùng thay đổi vị trí và hiển thị thời gian đã trôi qua của mục nội dung nghe nhìn. Để bật thanh tua, bạn phải triển khai PlaybackState.Builder#setActions và đưa vào ACTION_SEEK_TO.
| Khung giờ | Hành động | Tiêu chí |
|---|---|---|
| 1 | Phát |
Trạng thái hiện tại của là một trong những trạng thái sau:
PlaybackState
STATE_NONESTATE_STOPPEDSTATE_PAUSEDSTATE_ERROR |
| Vòng quay đang tải |
Trạng thái hiện tại của PlaybackState là một trong những trạng thái sau:
|
|
| Tạm dừng | Trạng thái hiện tại của PlaybackState không phải là trạng thái nào ở trên. |
|
| 2 | Trước | PlaybackState hành động bao gồm ACTION_SKIP_TO_PREVIOUS. |
| Tùy chỉnh | PlaybackState hành động không bao gồm ACTION_SKIP_TO_PREVIOUS và PlaybackState hành động tuỳ chỉnh bao gồm một hành động tuỳ chỉnh chưa được đặt. |
|
| Trống | PlaybackState phần bổ sung bao gồm giá trị boolean true cho khoá SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV. |
|
| 3 | Tiếp theo | PlaybackState hành động bao gồm ACTION_SKIP_TO_NEXT. |
| Tùy chỉnh | PlaybackState hành động không bao gồm ACTION_SKIP_TO_NEXT và PlaybackState hành động tuỳ chỉnh bao gồm một hành động tuỳ chỉnh chưa được đặt. |
|
| Trống | PlaybackState phần bổ sung bao gồm giá trị boolean true cho khoá SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT. |
|
| 4 | Tùy chỉnh | PlaybackState các hành động tuỳ chỉnh bao gồm một hành động tuỳ chỉnh chưa được đặt. |
| 5 | Tùy chỉnh | PlaybackState các hành động tuỳ chỉnh bao gồm một hành động tuỳ chỉnh chưa được đặt. |
Thêm các hành động tiêu chuẩn
Các ví dụ về mã sau đây minh hoạ cách thêm các hành động tiêu chuẩn và tuỳ chỉnh PlaybackState.
Đối với các hành động phát, tạm dừng, trước và tiếp theo, hãy đặt các hành động này trong PlaybackState cho phiên nội dung nghe nhìn.
Kotlin
val session = MediaSessionCompat(context, TAG) val playbackStateBuilder = PlaybackStateCompat.Builder() val style = NotificationCompat.MediaStyle() // For this example, the media is currently paused: val state = PlaybackStateCompat.STATE_PAUSED val position = 0L val playbackSpeed = 1f playbackStateBuilder.setState(state, position, playbackSpeed) // And the user can play, skip to next or previous, and seek val stateActions = PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar playbackStateBuilder.setActions(stateActions) // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()) style.setMediaSession(session.sessionToken) notificationBuilder.setStyle(style)
Java
MediaSessionCompat session = new MediaSessionCompat(context, TAG); PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder(); NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle(); // For this example, the media is currently paused: int state = PlaybackStateCompat.STATE_PAUSED; long position = 0L; float playbackSpeed = 1f; playbackStateBuilder.setState(state, position, playbackSpeed); // And the user can play, skip to next or previous, and seek long stateActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb playbackStateBuilder.setActions(stateActions); // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()); style.setMediaSession(session.getSessionToken()); notificationBuilder.setStyle(style);
Nếu không muốn có nút nào trong các vùng trước hoặc tiếp theo, đừng thêm ACTION_SKIP_TO_PREVIOUS hoặc ACTION_SKIP_TO_NEXT, mà hãy thêm các phần bổ sung vào phiên:
Kotlin
session.setExtras(Bundle().apply { putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) })
Java
Bundle extras = new Bundle(); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true); session.setExtras(extras);
Thêm thao tác tuỳ chỉnh
Đối với các hành động khác mà bạn muốn hiển thị trên các nút điều khiển nội dung nghe nhìn, bạn có thể tạo
PlaybackStateCompat.CustomAction
và thêm hành động đó vào PlaybackState thay vì. Các hành động này sẽ xuất hiện theo thứ tự được thêm.
Kotlin
val customAction = PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build() playbackStateBuilder.addCustomAction(customAction)
Java
PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build(); playbackStateBuilder.addCustomAction(customAction);
Phản hồi các hành động PlaybackState
Khi người dùng nhấn vào một nút, SystemUI sẽ sử dụng
MediaController.TransportControls
để gửi lệnh trở lại MediaSession. Bạn cần đăng ký một lệnh gọi lại có thể phản hồi đúng cách đối với các sự kiện này.
Kotlin
val callback = object: MediaSession.Callback() { override fun onPlay() { // start playback } override fun onPause() { // pause playback } override fun onSkipToPrevious() { // skip to previous } override fun onSkipToNext() { // skip to next } override fun onSeekTo(pos: Long) { // jump to position in track } override fun onCustomAction(action: String, extras: Bundle?) { when (action) { CUSTOM_ACTION_1 -> doCustomAction1(extras) CUSTOM_ACTION_2 -> doCustomAction2(extras) else -> { Log.w(TAG, "Unknown custom action $action") } } } } session.setCallback(callback)
Java
MediaSession.Callback callback = new MediaSession.Callback() { @Override public void onPlay() { // start playback } @Override public void onPause() { // pause playback } @Override public void onSkipToPrevious() { // skip to previous } @Override public void onSkipToNext() { // skip to next } @Override public void onSeekTo(long pos) { // jump to position in track } @Override public void onCustomAction(String action, Bundle extras) { if (action.equals(CUSTOM_ACTION_1)) { doCustomAction1(extras); } else if (action.equals(CUSTOM_ACTION_2)) { doCustomAction2(extras); } else { Log.w(TAG, "Unknown custom action " + action); } } };
Tiếp tục phát nội dung nghe nhìn
Để ứng dụng trình phát của bạn xuất hiện trong vùng cài đặt nhanh, bạn phải tạo thông báo MediaStyle bằng mã thông báo MediaSession hợp lệ.
Để hiển thị tiêu đề cho thông báo MediaStyle, hãy sử dụng NotificationBuilder.setContentTitle().
Để hiển thị biểu tượng thương hiệu cho trình phát nội dung nghe nhìn, hãy sử dụng NotificationBuilder.setSmallIcon().
Để hỗ trợ tính năng tiếp tục phát, các ứng dụng phải triển khai MediaBrowserService và MediaSession. MediaSession phải triển khai lệnh gọi lại onPlay().
Triển khai MediaBrowserService
Sau khi thiết bị khởi động, hệ thống sẽ tìm 5 ứng dụng nội dung nghe nhìn được sử dụng gần đây nhất và cung cấp các nút điều khiển có thể dùng để khởi động lại quá trình phát từ mỗi ứng dụng.
Hệ thống cố gắng liên hệ với MediaBrowserService bằng một kết nối từ SystemUI. Ứng dụng của bạn phải cho phép các kết nối như vậy, nếu không, ứng dụng sẽ không thể hỗ trợ tính năng tiếp tục phát.
Bạn có thể xác định và xác minh các kết nối từ SystemUI bằng tên gói com.android.systemui và chữ ký. SystemUI được ký bằng chữ ký nền tảng. Bạn có thể tìm thấy ví dụ về cách kiểm tra chữ ký nền tảng trong ứng dụng UAMP.
Để hỗ trợ tính năng tiếp tục phát, MediaBrowserService phải triển khai các hành vi sau:
onGetRoot()phải nhanh chóng trả về một gốc không rỗng. Bạn nên xử lý logic phức tạp khác trongonLoadChildren()Khi
onLoadChildren()được gọi trên mã nội dung nghe nhìn gốc, kết quả phải chứa một FLAG_PLAYABLE phần tử con.MediaBrowserServicesẽ trả về mục nội dung nghe nhìn được phát gần đây nhất khi nhận được truy vấn EXTRA_RECENT. Giá trị trả về phải là một mục nội dung nghe nhìn thực tế chứ không phải là hàm chung.MediaBrowserServicephải cung cấp MediaDescription thích hợp với tiêu đề và phụ đề không trống. Ứng dụng này cũng phải đặt URI biểu tượng hoặc bitmap biểu tượng.
Các ví dụ về mã sau đây minh hoạ cách triển khai onGetRoot().
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { rootHints?.let { if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. val extras = Bundle().apply { putBoolean(BrowserRoot.EXTRA_RECENT, true) } return BrowserRoot(MY_RECENTS_ROOT_ID, extras) } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return BrowserRoot(MY_MEDIA_ROOT_ID, null) } // Return an empty tree to disallow browsing. return BrowserRoot(MY_EMPTY_ROOT_ID, null)
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { if (rootHints != null) { if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. Bundle extras = new Bundle(); extras.putBoolean(BrowserRoot.EXTRA_RECENT, true); return new BrowserRoot(MY_RECENTS_ROOT_ID, extras); } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } // Return an empty tree to disallow browsing. return new BrowserRoot(MY_EMPTY_ROOT_ID, null); }
Hành vi trước Android 13
Để đảm bảo khả năng tương thích ngược, Giao diện người dùng hệ thống tiếp tục cung cấp một bố cục thay thế sử dụng các hành động thông báo cho các ứng dụng không cập nhật để nhắm đến Android 13 hoặc không bao gồm thông tin PlaybackState. Các nút hành động được lấy từ danh sách Notification.Action được đính kèm vào thông báo MediaStyle. Hệ thống hiển thị tối đa 5 hành động theo thứ tự được thêm. Ở chế độ thu gọn, tối đa 3 nút sẽ xuất hiện, được xác định bằng các
giá trị được truyền vào
setShowActionsInCompactView().
Các hành động tuỳ chỉnh được đặt theo thứ tự thêm vào PlaybackState.
Ví dụ về mã sau đây minh hoạ cách thêm các hành động vào thông báo MediaStyle :
Kotlin
import androidx.core.app.NotificationCompat import androidx.media3.session.MediaStyleNotificationHelper var notification = NotificationCompat.Builder(context, CHANNEL_ID) // Show controls on lock screen even when user hides sensitive content. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) // Add media control buttons that invoke intents in your media service .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 // Apply the media style template .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build()
Java
import androidx.core.app.NotificationCompat; import androidx.media3.session.MediaStyleNotificationHelper; NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID) // Show controls on lock screen even when user hides sensitive content. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) // Add media control buttons that invoke intents in your media service .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 // Apply the media style template .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();