Aplikasi Anda harus mendeklarasikan MediaBrowserService
dengan filter intent dalam manifesnya. Anda dapat memilih nama layanan Anda sendiri; dalam contoh berikut, nama layanan yang digunakan adalah "MediaPlaybackService".
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Catatan: Implementasi yang direkomendasikan untuk MediaBrowserService
adalah MediaBrowserServiceCompat
.
yang ditentukan dalam support library media-compat.
Di sepanjang artikel ini, istilah "MediaBrowserService" merujuk ke instance dari MediaBrowserServiceCompat
.
Menginisialisasi sesi media
Saat menerima metode callback siklus proses onCreate()
, layanan harus melakukan langkah-langkah berikut:
- Membuat dan menginisialisasi sesi media
- Menetapkan callback sesi media
- Menetapkan token sesi media
Kode onCreate()
di bawah ini menunjukkan langkah-langkah tersebut:
Kotlin
private const val MY_MEDIA_ROOT_ID = "media_root_id" private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id" class MediaPlaybackService : MediaBrowserServiceCompat() { private var mediaSession: MediaSessionCompat? = null private lateinit var stateBuilder: PlaybackStateCompat.Builder override fun onCreate() { super.onCreate() // Create a MediaSessionCompat mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply { // Enable callbacks from MediaButtons and TransportControls setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ) // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE ) setPlaybackState(stateBuilder.build()) // MySessionCallback() has methods that handle callbacks from a media controller setCallback(MySessionCallback()) // Set the session's token so that client activities can communicate with it. setSessionToken(sessionToken) } } }
Java
public class MediaPlaybackService extends MediaBrowserServiceCompat { private static final String MY_MEDIA_ROOT_ID = "media_root_id"; private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"; private MediaSessionCompat mediaSession; private PlaybackStateCompat.Builder stateBuilder; @Override public void onCreate() { super.onCreate(); // Create a MediaSessionCompat mediaSession = new MediaSessionCompat(context, LOG_TAG); // Enable callbacks from MediaButtons and TransportControls mediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE); mediaSession.setPlaybackState(stateBuilder.build()); // MySessionCallback() has methods that handle callbacks from a media controller mediaSession.setCallback(new MySessionCallback()); // Set the session's token so that client activities can communicate with it. setSessionToken(mediaSession.getSessionToken()); } }
Mengelola koneksi klien
MediaBrowserService
memiliki dua metode yang menangani koneksi klien: onGetRoot()
mengontrol akses ke layanan, dan onLoadChildren()
menyediakan kemampuan bagi klien untuk membuat dan menampilkan menu hierarki konten .
Mengontrol koneksi klien dengan onGetRoot()
Metode onGetRoot()
menampilkan node root hierarki konten. Jika metode ini menampilkan null, berarti koneksi ditolak.
Agar klien dapat terhubung ke layanan Anda dan menjelajahi konten medianya, onGetRoot() harus menampilkan BrowserRoot bukan null yang merupakan ID root yang mewakili hierarki konten Anda.
Agar klien dapat terhubung ke MediaSession tanpa melakukan penjelajahan, onGetRoot() tetap harus menampilkan BrowserRoot bukan null, tetapi ID root ini harus mewakili hierarki konten yang kosong.
Implementasi standar onGetRoot()
mungkin terlihat seperti ini:
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): MediaBrowserServiceCompat.BrowserRoot { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. return if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null) } else { // Clients can connect, but this BrowserRoot is an empty hierachy // so onLoadChildren returns nothing. This disables the ability to browse for content. MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null) } }
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } else { // Clients can connect, but this BrowserRoot is an empty hierachy // so onLoadChildren returns nothing. This disables the ability to browse for content. return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null); } }
Dalam beberapa kasus, Anda mungkin perlu menerapkan skema whitelist/blacklist untuk mengontrol koneksi. Untuk contoh pembuatan whitelist, lihat class PackageValidator dalam contoh aplikasi Universal Android Music Player.
Sebaiknya pertimbangkan untuk menyediakan hierarki konten yang berbeda, sesuai jenis klien yang membuat kueri tersebut. Secara khusus, Android Auto membatasi interaksi pengguna dengan aplikasi audio. Untuk informasi selengkapnya, lihat Memutar Audio untuk Auto. Anda dapat melihat clientPackageName
pada waktu koneksi untuk menentukan jenis klien, dan menampilkan BrowserRoot
berbeda tergantung kliennya (atau rootHints
, jika ada).
Mengomunikasikan konten dengan onLoadChildren()
Setelah tersambung, klien dapat menjelajahi hierarki konten dengan melakukan panggilan berulang ke MediaBrowserCompat.subscribe()
untuk membuat representasi lokal dari UI. Metode subscribe()
mengirim callback onLoadChildren()
ke layanan, yang akan menampilkan daftar objek MediaBrowser.MediaItem
.
Setiap MediaItem memiliki string ID unik, yang merupakan token buram. Saat klien ingin membuka submenu atau memutar item, ID tersebut akan diteruskan. Layanan Anda bertanggung jawab untuk mengaitkan ID ini dengan node menu atau item konten yang sesuai.
Implementasi sederhana dari onLoadChildren()
mungkin terlihat seperti ini:
Kotlin
override fun onLoadChildren( parentMediaId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> ) { // Browsing not allowed if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) { result.sendResult(null) return } // Assume for example that the music catalog is already loaded/cached. val mediaItems = emptyList<MediaBrowserCompat.MediaItem>() // Check if this is the root menu: if (MY_MEDIA_ROOT_ID == parentMediaId) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems) }
Java
@Override public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { // Browsing not allowed if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) { result.sendResult(null); return; } // Assume for example that the music catalog is already loaded/cached. List<MediaItem> mediaItems = new ArrayList<>(); // Check if this is the root menu: if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems); }
Catatan: Objek MediaItem
yang dikirim oleh MediaBrowserService tidak boleh berisi bitmap ikon. Sebagai gantinya, gunakan Uri
dengan memanggil setIconUri()
saat Anda membuat MediaDescription
untuk setiap item.
Untuk cara mengimplementasikan onLoadChildren()
, lihat contoh aplikasi Universal Android Music Player.
Siklus hidup layanan browser media
Perilaku layanan Android bergantung pada apakah layanan tersebut dimulai atau terikat ke satu atau beberapa klien. Setelah dibuat, layanan dapat berstatus dimulai, terikat, atau keduanya. Dalam semua status ini, layanan berfungsi sepenuhnya dan dapat menjalankan tugas yang ditetapkan padanya. Perbedaannya terletak pada berapa lama layanan itu akan bertahan. Layanan terikat tidak akan dimusnahkan hingga semua klien terikatnya terbebas. Layanan yang dimulai dapat dihentikan dan dimusnahkan secara eksplisit (dengan asumsi layanan tersebut tidak terikat lagi dengan klien mana pun).
Ketika MediaBrowser
yang berjalan di aktivitas lain terhubung ke MediaBrowserService
, aktivitas akan diikat ke layanan, sehingga layanan menjadi terikat (tetapi tidak dimulai). Perilaku default ini tertanam dalam class MediaBrowserServiceCompat
.
Layanan yang hanya terikat (dan tidak dimulai) dimusnahkan setelah semua kliennya terbebas. Jika aktivitas UI Anda terputus pada tahap ini, layanan akan dimusnahkan. Hal ini tidak menjadi masalah jika Anda belum memutar musik apa pun. Namun, jika pemutaran sudah dimulai, pengguna mungkin berharap untuk terus mendengarkan musik meskipun mereka telah beralih aplikasi. Anda tidak boleh memusnahkan pemutar ketika membebaskan UI agar berfungsi dengan aplikasi lain.
Karena alasan ini, Anda perlu memastikan bahwa layanan dimulai saat layanan tersebut memulai pemutaran dengan memanggil startService()
. Layanan yang dimulai harus dihentikan secara eksplisit, terlepas dari apakah layanan tersebut terikat atau tidak. Hal ini memastikan bahwa pemutar tetap berjalan meskipun aktivitas UI pengontrolnya tidak terikat lagi.
Untuk menghentikan layanan yang dimulai, panggil Context.stopService()
atau stopSelf()
. Sistem akan menghentikan dan memusnahkan layanan sesegera mungkin. Namun, jika satu atau beberapa klien masih terikat ke layanan, panggilan untuk menghentikan layanan akan ditunda sampai semua kliennya terbebas.
Siklus proses MediaBrowserService
dikendalikan berdasarkan cara pembuatannya, jumlah klien yang terikat padanya, dan panggilan yang diterimanya dari callback sesi media. Ringkasnya:
- Layanan dibuat saat dimulai sebagai respons terhadap tombol media atau ketika sebuah aktivitas menjadi terikat padanya (setelah tersambung melalui
MediaBrowser
-nya). - Callback
onPlay()
sesi media harus menyertakan kode yang memanggilstartService()
. Hal ini memastikan bahwa layanan dimulai dan terus berjalan meskipun semua aktivitasMediaBrowser
UI yang terikat padanya telah terbebas. - Callback
onStop()
harus memanggilstopSelf()
. Jika layanan dimulai, panggilan ini akan menghentikannya. Selain itu, layanan akan dimusnahkan jika tidak ada aktivitas yang terikat padanya. Jika sebaliknya, layanan akan tetap terikat hingga semua aktivitasnya terbebas. (Jika panggilanstartService()
berikutnya diterima sebelum layanan dimusnahkan, penghentian yang tertunda akan dibatalkan.)
Diagram alir berikut menunjukkan cara mengelola siklus proses layanan. Penghitung variabel melacak jumlah klien terikat:
Menggunakan notifikasi MediaStyle dengan layanan latar depan
Saat dijalankan, layanan seharusnya berjalan di latar depan. Dengan begitu sistem akan mengetahui bahwa layanan tersebut menjalankan fungsi yang berguna dan tidak akan diakhiri jika sistem hampir kehabisan memori. Layanan latar depan harus menampilkan notifikasi agar pengguna mengetahuinya dan dapat mengendalikannya, jika ingin. Callback onPlay()
harus menempatkan layanan di latar depan. (Perhatikan bahwa ini adalah makna khusus dari "latar depan". Bagi Android, layanan dianggap berjalan di latar depan untuk tujuan pengelolaan proses. Sebaliknya, bagi pengguna, layanan dianggap memutar media di latar belakang sementara beberapa aplikasi lainnya tampak di "latar depan" pada layar.)
Saat berjalan di latar depan, layanan harus menampilkan notifikasi, yang idealnya berisi satu atau beberapa kontrol transport. Notifikasi ini juga harus menyertakan informasi berguna dari metadata sesi.
Buat dan tampilkan notifikasi saat pemutar mulai memutar media. Tempat terbaik untuk melakukannya adalah di dalam metode MediaSessionCompat.Callback.onPlay()
.
Contoh di bawah ini menggunakan NotificationCompat.MediaStyle
, yang dirancang untuk aplikasi media. Contoh ini menunjukkan cara membuat notifikasi yang menampilkan kontrol transport dan metadata. Metode praktis getController()
memungkinkan Anda membuat pengontrol media langsung dari sesi media.
Kotlin
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata val controller = mediaSession.controller val mediaMetadata = controller.metadata val description = mediaMetadata.description val builder = NotificationCompat.Builder(context, channelId).apply { // Add the metadata for the currently playing track setContentTitle(description.title) setContentText(description.subtitle) setSubText(description.description) setLargeIcon(description.iconBitmap) // Enable launching the player by clicking the notification setContentIntent(controller.sessionActivity) // Stop the service when the notification is swiped away setDeleteIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) // Make the transport controls visible on the lockscreen setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color setSmallIcon(R.drawable.notification_icon) color = ContextCompat.getColor(context, R.color.primaryDark) // Add a pause button addAction( NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_PLAY_PAUSE ) ) ) // Take advantage of MediaStyle features setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) ) } // Display the notification and place the service in the foreground startForeground(id, builder.build())
Java
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); builder // Add the metadata for the currently playing track .setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSubText(description.getDescription()) .setLargeIcon(description.getIconBitmap()) // Enable launching the player by clicking the notification .setContentIntent(controller.getSessionActivity()) // Stop the service when the notification is swiped away .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)) // Make the transport controls visible on the lockscreen .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(context, R.color.primaryDark)) // Add a pause button .addAction(new NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE))) // Take advantage of MediaStyle features .setStyle(new MediaStyle() .setMediaSession(mediaSession.getSessionToken()) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))); // Display the notification and place the service in the foreground startForeground(id, builder.build());
Saat menggunakan notifikasi MediaStyle, perhatikan perilaku setelan NotificationCompat berikut:
- Saat Anda menggunakan
setContentIntent()
, layanan Anda akan otomatis dimulai saat notifikasi diklik. - Dalam situasi "tidak tepercaya" seperti di layar kunci, visibilitas default untuk konten notifikasi adalah
VISIBILITY_PRIVATE
. Anda mungkin ingin melihat kontrol transport di layar kunci, jadiVISIBILITY_PUBLIC
merupakan opsi yang tepat. - Berhati-hatilah saat menyetel warna latar belakang. Pada notifikasi biasa di Android versi 5.0 atau yang lebih baru, warna hanya diterapkan ke latar belakang ikon aplikasi kecil. Namun, untuk notifikasi MediaStyle di Android versi sebelum 7.0, warna digunakan untuk seluruh latar belakang notifikasi. Uji warna latar belakang Anda. Perhatikan kenyamanan mata pengguna dan hindari warna yang sangat terang atau berpendar.
Setelan ini hanya tersedia jika Anda menggunakan NotificationCompat.MediaStyle:
- Gunakan
setMediaSession()
untuk mengaitkan notifikasi dengan sesi Anda. Hal ini memungkinkan aplikasi pihak ketiga dan perangkat pendamping untuk mengakses dan mengontrol sesi. - Gunakan
setShowActionsInCompactView()
untuk menambahkan hingga 3 tindakan yang akan ditampilkan di contentView ukuran standar pada notifikasi. (Di sini tombol jeda ditentukan.) - Di Android 5.0 (API level 21) dan yang lebih baru, Anda dapat menggeser notifikasi untuk menghentikan pemutar setelah layanan tidak lagi berjalan di latar depan. Anda tidak dapat melakukan tindakan ini di versi Android yang lebih lama. Agar pengguna dapat menghapus pemberitahuan dan menghentikan pemutaran pada Android sebelum versi 5.0 (API level 21), Anda dapat menambahkan tombol batal di pojok kanan atas notifikasi dengan memanggil
setShowCancelButton(true)
dansetCancelButtonIntent()
.
Saat menambahkan tombol jeda dan batal, Anda perlu PendingIntent untuk dikaitkan ke tindakan pemutaran. Metode MediaButtonReceiver.buildMediaButtonPendingIntent()
menangani konversi tindakan PlaybackState menjadi PendingIntent.