Google berkomitmen untuk mendorong terwujudnya keadilan ras bagi komunitas Kulit Hitam. Lihat caranya.

Membuat layanan browser media

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:

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 memanggil startService(). Hal ini memastikan bahwa layanan dimulai dan terus berjalan meskipun semua aktivitas MediaBrowser UI yang terikat padanya telah terbebas.
  • Callback onStop() harus memanggil stopSelf(). 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 panggilan startService() 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:

Siklus Proses Layanan

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, jadi VISIBILITY_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) dan setCancelButtonIntent().

Saat menambahkan tombol jeda dan batal, Anda perlu PendingIntent untuk dikaitkan ke tindakan pemutaran. Metode MediaButtonReceiver.buildMediaButtonPendingIntent() menangani konversi tindakan PlaybackState menjadi PendingIntent.