إنشاء خدمة لمتصفّح الوسائط

يجب أن يعلن تطبيقك عن MediaBrowserService باستخدام intent-filter في ملف البيان. يمكنك اختيار اسم الخدمة بنفسك. في المثال التالي، اسم الخدمة الذي تم اختياره هو MediaPlaybackService.

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

تهيئة جلسة الوسائط

عندما تتلقّى الخدمة طريقة معاودة الاتصال بدورة الحياة onCreate()، يجب أن تنفّذ الخطوات التالية:

  • إنشاء جلسة الوسائط وتهيئتها
  • ضبط معاودة الاتصال بجلسة الوسائط
  • ضبط الرمز المميز لجلسة الوسائط

يوضّح الرمز onCreate() التالي هذه الخطوات:

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());
    }
}

إدارة اتصالات العملاء

تحتوي MediaBrowserService على طريقتَين للتعامل مع اتصالات العملاء: تتحكّم الطريقة onGetRoot() في الوصول إلى الخدمة، وتوفّر الطريقة onLoadChildren() للعميل إمكانية إنشاء وعرض قائمة بالتسلسل الهرمي لمحتوى MediaBrowserService.

التحكّم في اتصالات العملاء باستخدام onGetRoot()

تعرض الطريقة onGetRoot() العقدة الجذرية للتسلسل الهرمي للمحتوى. إذا عرضت الطريقة قيمة فارغة، يتم رفض الاتصال.

للسماح للعملاء بالاتصال بخدمتك وتصفّح محتواها من الوسائط، يجب أن تعرض onGetRoot() عنصر BrowserRoot غير فارغ، وهو معرّف جذر يمثّل التسلسل الهرمي للمحتوى.

للسماح للعملاء بالاتصال بـ MediaSession بدون تصفّح، يجب أن تعرض onGetRoot() عنصر BrowserRoot غير فارغ، ولكن يجب أن يمثّل معرّف الجذر تسلسلاً هرميًا فارغًا للمحتوى.

قد يبدو التنفيذ النموذجي للطريقة onGetRoot() على النحو التالي:

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);
    }
}

في بعض الحالات، قد تريد التحكّم في المستخدمين الذين يمكنهم الاتصال بـ MediaBrowserService. إحدى الطرق هي استخدام قائمة التحكّم في الوصول (ACL) التي تحدّد الاتصالات المسموح بها، أو بدلاً من ذلك، تحدّد الاتصالات التي يجب منعها. للاطّلاع على مثال حول كيفية تنفيذ قائمة التحكّم في الوصول التي تسمح باتصالات معيّنة، يمكنك الاطّلاع على فئة PackageValidator في نموذج تطبيق Universal Android Music Player.

ننصحك بتوفير تسلسلات هرمية مختلفة للمحتوى استنادًا إلى نوع العميل الذي يُجري طلب البحث. على وجه الخصوص، يحدّ Android Auto من طريقة تفاعل المستخدمين مع تطبيقات الصوت. لمزيد من المعلومات، يمكنك الاطّلاع على مقالة تشغيل الصوت في Android Auto. يمكنك الاطّلاع على clientPackageName في وقت الاتصال لتحديد نوع العميل، وعرض BrowserRoot مختلف استنادًا إلى العميل (أو rootHints إذا كانت متوفّرة).

إرسال المحتوى باستخدام onLoadChildren()

بعد اتصال العميل، يمكنه الانتقال إلى التسلسل الهرمي للمحتوى من خلال إجراء طلبات متكررة إلى MediaBrowserCompat.subscribe() لإنشاء تمثيل محلي لواجهة المستخدم. ترسل الطريقة subscribe() معاودة الاتصال onLoadChildren() إلى الخدمة، التي تعرض قائمة بعناصر MediaBrowser.MediaItem.

يحتوي كل MediaItem على سلسلة معرّف فريدة، وهي رمز غير شفاف. عندما يريد العميل فتح قائمة فرعية أو تشغيل عنصر، يمرِّر المعرّف. تكون خدمتك مسؤولة عن ربط المعرّف بعقدة القائمة أو عنصر المحتوى المناسبَين.

قد يبدو التنفيذ البسيط للطريقة onLoadChildren() على النحو التالي:

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);
}

ملاحظة: MediaItem يجب ألا تحتوي العناصر التي تقدّمها MediaBrowserService على صور رمزية. استخدِم Uri بدلاً من ذلك من خلال استدعاء setIconUri() عند إنشاء MediaDescription لكل عنصر.

للاطّلاع على مثال حول كيفية تنفيذ onLoadChildren()، يمكنك الاطّلاع على نموذج تطبيق Universal Android Music Player.

دورة حياة خدمة متصفّح الوسائط

يعتمد سلوك خدمة Android على ما إذا كانت بدأت أو مرتبطة بعميل واحد أو أكثر. بعد إنشاء الخدمة، يمكن بدءها أو ربطها أو كليهما. في كل هذه الحالات، تكون الخدمة تعمل بكامل طاقتها ويمكنها تنفيذ العمل الذي تم تصميمها من أجله. والفرق هو المدة التي ستظل فيها الخدمة متاحة. لا يتم إيقاف خدمة مرتبطة إلى أن يتم إلغاء ربط جميع العملاء المرتبطين بها. يمكن إيقاف الخدمة التي تم بدءها وإيقافها بشكلٍ صريح (بافتراض أنّه لم يعُد أي عملاء مرتبطين بها).

عندما يتصل MediaBrowser قيد التشغيل في نشاط آخر بـ MediaBrowserService، يربط النشاط بالخدمة، ما يجعل الخدمة مرتبطة (ولكن لم يتم بدءها). هذا السلوك التلقائي مضمّن في فئة MediaBrowserServiceCompat.

يتم إيقاف الخدمة المرتبطة فقط (ولم يتم بدءها) عندما يتم إلغاء ربط جميع عملائها. إذا تم قطع اتصال نشاط واجهة المستخدم في هذه المرحلة، يتم إيقاف الخدمة. لا يمثّل ذلك مشكلة إذا لم تكن قد شغّلت أي موسيقى بعد. ومع ذلك، عند بدء التشغيل، من المحتمل أن يتوقّع المستخدم مواصلة الاستماع حتى بعد التبديل بين التطبيقات. لا تريد إيقاف مشغّل الوسائط عند إلغاء ربط واجهة المستخدم لاستخدام تطبيق آخر.

لهذا السبب، عليك التأكّد من بدء الخدمة عند بدء تشغيلها من خلال استدعاء startService(). يجب إيقاف الخدمة التي تم بدءها بشكلٍ صريح، سواء كانت مرتبطة أو لا. يضمن ذلك استمرار مشغّل الوسائط في العمل حتى إذا تم إلغاء ربط نشاط واجهة المستخدم المتحكّمة.

لإيقاف خدمة تم بدءها، استدعِ Context.stopService() أو stopSelf(). يوقف النظام الخدمة ويزيلها في أقرب وقت ممكن. ومع ذلك، إذا كان عميل واحد أو أكثر لا يزال مرتبطًا بالخدمة، يتم تأخير طلب إيقاف الخدمة إلى أن يتم إلغاء ربط جميع عملائها.

تتحكّم طريقة إنشاء MediaBrowserService وعدد العملاء المرتبطين بها والطلبات التي تتلقّاها من معاودات الاتصال بجلسة الوسائط في دورة حياة الخدمة. في ما يلي ملخّص:

  • يتم إنشاء الخدمة عند بدءها استجابةً لزر وسائط أو عندما يربط نشاط بها (بعد الاتصال عبر MediaBrowser).
  • يجب أن تتضمّن معاودة الاتصال onPlay() لجلسة الوسائط رمزًا يستدعي startService(). يضمن ذلك بدء الخدمة واستمرار تشغيلها، حتى عندما يتم إلغاء ربط جميع أنشطة MediaBrowser لواجهة المستخدم المرتبطة بها.
  • يجب أن تستدعي معاودة الاتصال onStop() الطريقة stopSelf(). إذا تم بدء الخدمة، يؤدي ذلك إلى إيقافها. بالإضافة إلى ذلك، يتم إيقاف الخدمة إذا لم تكن هناك أي أنشطة مرتبطة بها. بخلاف ذلك، تظل الخدمة مرتبطة إلى أن يتم إلغاء ربط جميع أنشطتها. (إذا تم تلقّي طلب startService() لاحق قبل إيقاف الخدمة، يتم إلغاء الإيقاف المعلق).

يوضّح مخطط التدفق التالي كيفية إدارة دورة حياة الخدمة. يتتبّع العداد المتغيّر عدد العملاء المرتبطين:

مراحل نشاط الخدمة

استخدام إشعارات MediaStyle مع خدمة تعمل في المقدّمة

عندما تكون الخدمة قيد التشغيل، يجب أن تعمل في المقدّمة. يُعلم ذلك النظام بأنّ الخدمة تؤدي وظيفة مفيدة ويجب عدم إيقافها إذا كان النظام يعاني من نقص في الذاكرة. يجب أن تعرض الخدمة التي تعمل في المقدّمة إشعارًا حتى يعرف المستخدم عنها ويمكنه التحكّم فيها اختياريًا. يجب أن تضع معاودة الاتصال onPlay() الخدمة في المقدّمة. (يُرجى العِلم أنّ هذا المعنى خاص بـ "المقدّمة". بينما يعتبر Android الخدمة في المقدّمة لأغراض إدارة العمليات، يرى المستخدم أنّ مشغّل الوسائط يعمل في الخلفية بينما يظهر تطبيق آخر في "المقدّمة" على الشاشة).

عندما تعمل الخدمة في المقدّمة، يجب أن تعرض إشعارًا، من الأفضل أن يتضمّن عنصر تحكّم واحدًا أو أكثر في النقل. يجب أن يتضمّن الإشعار أيضًا معلومات مفيدة من بيانات وصفية للجلسة.

أنشئ الإشعار واعرضه عندما يبدأ مشغّل الوسائط في التشغيل. أفضل مكان لإجراء ذلك هو داخل الطريقة MediaSessionCompat.Callback.onPlay().

يستخدِم المثال أدناه NotificationCompat.MediaStyle، المصمَّمة لتطبيقات الوسائط. يوضّح المثال كيفية إنشاء إشعار يعرض البيانات الوصفية وعناصر التحكّم في النقل. تسمح لك الطريقة الملائمة getController() بإنشاء وحدة تحكّم في الوسائط مباشرةً من جلسة الوسائط.

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());

عند استخدام إشعارات MediaStyle، يُرجى العِلم بسلوك إعدادات NotificationCompat التالية:

  • عند استخدام setContentIntent()، تبدأ خدمتك تلقائيًا عند النقر على الإشعار، وهي ميزة مفيدة.
  • في حالة "غير موثوق بها"، مثل شاشة القفل، تكون إعدادات مستوى رؤية محتوى الإشعار التلقائية هي VISIBILITY_PRIVATE. من المحتمل أن تريد رؤية عناصر التحكّم في النقل على شاشة القفل، لذا فإنّ VISIBILITY_PUBLIC هي الخيار المناسب.
  • يُرجى توخي الحذر عند ضبط لون الخلفية. في إشعار عادي في الإصدار 5.0 من Android أو الإصدارات الأحدث، يتم تطبيق اللون على خلفية رمز التطبيق الصغير فقط. ولكن بالنسبة إلى إشعارات MediaStyle قبل الإصدار 7.0 من Android، يتم استخدام اللون لخلفية الإشعار بأكملها. اختبِر لون الخلفية. يُرجى اختيار ألوان مريحة للعين وتجنُّب الألوان الساطعة أو الفلورسنتية للغاية.

لا تتوفّر هذه الإعدادات إلا عند استخدام NotificationCompat.MediaStyle:

  • استخدِم setMediaSession() لربط الإشعار بجلسة الوسائط. يسمح ذلك لتطبيقات الجهات الخارجية والأجهزة المصاحبة بالوصول إلى الجلسة والتحكّم فيها.
  • استخدِم setShowActionsInCompactView() لإضافة ما يصل إلى 3 إجراءات ليتم عرضها في contentView بالحجم العادي للإشعار. (تم هنا تحديد زر الإيقاف المؤقت).
  • في الإصدار 5.0 من Android (مستوى واجهة برمجة التطبيقات 21) والإصدارات الأحدث، يمكنك إيقاف مشغّل الوسائط بعد إيقاف الخدمة التي تعمل في المقدّمة من خلال إزالة الإشعار. لا يمكنك إجراء ذلك في الإصدارات السابقة. للسماح للمستخدمين بإزالة الإشعار وإيقاف التشغيل قبل الإصدار 5.0 من Android (مستوى واجهة برمجة التطبيقات 21)، يمكنك إضافة زر إلغاء في أعلى يسار الإشعار من خلال استدعاء setShowCancelButton(true) وsetCancelButtonIntent().

عند إضافة زَري الإيقاف المؤقت والإلغاء، ستحتاج إلى PendingIntent لربطه بإجراء التشغيل. تتولّى الطريقة MediaButtonReceiver.buildMediaButtonPendingIntent() مهمة تحويل إجراء PlaybackState إلى PendingIntent.

تفعيل تصفّح الوسائط في AVRCP

بالإضافة إلى التطبيقات المخصّصة، مثل Android Auto، تعمل طبقة البلوتوث في النظام أيضًا كعميل لـ MediaBrowserService لتسهيل تصفّح الفهرس عن بُعد لاسلكيًا (AVRCP).

في الإصدارَين 16 و17 من Android، تتطلب المنصة من التطبيقات التي لا تستخدِم Media3 عرض نشاط معيّن باستخدام intent-filter ليتم التحقق منه للتصفّح.

أضِف intent-filter المحدّد هذا إلى نشاط تم تصديره في AndroidManifest.xml. يُرجى العِلم أنّه تم حذف CATEGORY_DEFAULT عمدًا لمنع ظهور تطبيقك في قوائم "فتح باستخدام" العامة لملفات الصوت المحلية:

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