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: Penerapan MediaBrowserService yang direkomendasikan adalah MediaBrowserServiceCompat. yang ditentukan dalam support library media-compat. Di seluruh halaman ini, istilah "MediaBrowserService" mengacu pada instance 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() memberikan kemampuan bagi klien untuk membuat dan menampilkan menu hierarki konten MediaBrowserService.

Mengontrol koneksi klien dengan onGetRoot()

Metode onGetRoot() menampilkan node root hierarki konten. Jika metode ini menampilkan null, koneksi akan ditolak.

Agar klien dapat terhubung ke layanan Anda dan menjelajahi konten medianya, onGetRoot() harus menampilkan BrowserRoot non-null, yaitu ID root yang mewakili hierarki konten Anda.

Agar klien dapat terhubung ke MediaSession tanpa menjelajah, onGetRoot() tetap harus menampilkan BrowserRoot non-null, tetapi ID root 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 hierarchy
        // 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 hierarchy
        // 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 ingin mengontrol siapa saja yang dapat terhubung ke MediaBrowserService. Salah satu caranya adalah dengan menggunakan daftar kontrol akses (ACL) yang menentukan koneksi mana yang diizinkan, atau menghitung koneksi mana yang harus dilarang. Untuk contoh cara menerapkan ACL yang memungkinkan koneksi tertentu, lihat class PackageValidator di aplikasi contoh Universal Android Music Player.

Sebaiknya pertimbangkan untuk menyediakan hierarki konten yang berbeda, tergantung pada jenis klien yang membuat kueri. Secara khusus, Android Auto membatasi cara pengguna berinteraksi dengan aplikasi audio. Untuk informasi selengkapnya, lihat Memutar Audio untuk Otomatis. Anda dapat melihat clientPackageName pada waktu koneksi untuk menentukan jenis klien, dan menampilkan BrowserRoot yang berbeda bergantung pada 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 mem-build 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 harus memastikan layanan dimulai saat mulai diputar dengan memanggil startService(). Layanan yang dimulai harus dihentikan secara eksplisit, terlepas dari apakah layanan tersebut terikat atau tidak. Hal ini memastikan bahwa pemutar Anda akan terus berperforma meskipun aktivitas UI pengendalian terlepas.

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 didesain 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. Ini adalah fitur yang berguna.
  • Dalam situasi "tidak tepercaya" seperti layar kunci, visibilitas default untuk konten notifikasi adalah VISIBILITY_PRIVATE. Anda mungkin ingin melihat kontrol transpor di layar kunci, jadi VISIBILITY_PUBLIC adalah pilihan yang tepat.
  • Berhati-hatilah saat menyetel warna latar belakang. Dalam notifikasi biasa di Android versi 5.0 atau yang lebih baru, warna hanya diterapkan ke latar belakang ikon aplikasi kecil. Namun, untuk notifikasi MediaStyle sebelum Android 7.0, warna digunakan untuk seluruh latar belakang notifikasi. Uji warna latar belakang Anda. Berikan rasa lembut pada mata 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 mengakses dan mengontrol sesi.
  • Gunakan setShowActionsInCompactView() untuk menambahkan hingga 3 tindakan yang akan ditampilkan di contentView berukuran standar notifikasi. (Di sini tombol jeda ditentukan.)
  • Di Android 5.0 (API level 21) dan yang lebih baru, Anda dapat menggeser notifikasi untuk menghentikan pemain setelah layanan tidak lagi berjalan di latar depan. Anda tidak dapat melakukan hal ini di versi sebelumnya. Agar pengguna dapat menghapus notifikasi dan menghentikan pemutaran sebelum Android 5.0 (API level 21), Anda dapat menambahkan tombol batal di sudut kanan atas notifikasi dengan memanggil setShowCancelButton(true) dan setCancelButtonIntent().

Saat menambahkan tombol jeda dan batal, Anda memerlukan PendingIntent untuk dilampirkan ke tindakan pemutaran. Metode MediaButtonReceiver.buildMediaButtonPendingIntent() melakukan tugas konversi tindakan PlaybackState menjadi PendingIntent.