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 dipilih adalah MediaPlaybackService.

<service android:name=".MediaPlaybackService">
  <intent-filter>
    <action android:name="android.media.browse.MediaBrowserService" />
  </intent-filter>
</service>

Menginisialisasi sesi media

Saat menerima metode callback siklus proses onCreate(), layanan harus melakukan langkah-langkah berikut:

Kode onCreate() berikut menunjukkan langkah-langkah ini:

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 MediaBrowserService.

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 Anda 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 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 yang dapat terhubung ke MediaBrowserService Anda. Salah satu caranya adalah dengan menggunakan daftar kontrol akses (ACL) yang menentukan koneksi mana yang diizinkan, atau secara alternatif mencantumkan koneksi mana yang harus dilarang. Untuk contoh cara mengimplementasikan ACL yang mengizinkan koneksi tertentu, lihat class PackageValidator di 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 mengetahui 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 terhubung, klien dapat menjelajahi hierarki konten dengan melakukan panggilan berulang ke MediaBrowserCompat.subscribe() untuk membuat representasi lokal 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 contoh cara mengimplementasikan onLoadChildren(), lihat aplikasi contoh 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 Anda terus 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 notifikasi dan menghentikan pemutaran sebelum Android 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 memerlukan PendingIntent untuk dikaitkan ke tindakan pemutaran. Metode MediaButtonReceiver.buildMediaButtonPendingIntent() menangani konversi tindakan PlaybackState menjadi PendingIntent.

Mengaktifkan penjelajahan media AVRCP

Selain aplikasi kustom seperti Android Auto, lapisan Bluetooth sistem juga berfungsi sebagai klien ke MediaBrowserService Anda untuk memfasilitasi penjelajahan katalog jarak jauh nirkabel (AVRCP).

Di Android 16 dan Android 17, platform mewajibkan aplikasi yang tidak menggunakan Media3 mengekspos aktivitas tertentu dengan filter intent agar divalidasi untuk penjelajahan.

Tambahkan filter intent khusus ini ke aktivitas yang diekspor di AndroidManifest.xml. Perhatikan bahwa CATEGORY_DEFAULT sengaja tidak disertakan untuk mencegah aplikasi Anda muncul di menu "Buka dengan" umum untuk file audio lokal:

<activity
    android:name=".BluetoothValidationActivity"
    android:exported="true"
    android:theme="@android:style/Theme.NoDisplay"
    android:excludeFromRecents="true"
    android:noHistory="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="content" />
    <data android:host="media" />
    <!-- Specific path check used by Bluetooth stack for validation -->
    <data android:pathPrefix="/internal/audio/media/" />
    <data android:mimeType="audio/*" />
  </intent-filter>
</activity>