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

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

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

ملاحظة: التنفيذ المُقترَح للسمة MediaBrowserService هو MediaBrowserServiceCompat. وهو محدَّد في مكتبة دعم Media-compat في هذه الصفحة، يشير المصطلح "MediaBrowserService" إلى مثيل MediaBrowserServiceCompat.

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

عندما تتلقّى الخدمة طريقة معاودة الاتصال في "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() قيمة معرِّف جذر غير فارغة، وهي عبارة عن معرِّف جذر يمثّل التدرّج الهرمي للمحتوى.

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

قد يبدو التنفيذ النموذجي للسمة 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) التي تحدد الاتصالات المسموح بها، أو بدلاً من ذلك تضم الاتصالات التي يجب حظرها. للحصول على مثال حول كيفية تنفيذ قائمة تحكم في الوصول تسمح باتصالات محددة، يمكنك الاطّلاع على فئة PackageHealthator في نموذج تطبيق Universal Android Music Player.

ننصحك بتقديم تسلسلات هرمية مختلفة للمحتوى اعتمادًا على نوع العميل الذي يُجري الاستعلام. وبشكل خاص، يقيّد 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 التي تسبق الإصدار Android 7.0، فيتم استخدام اللون لخلفية الإشعارات بالكامل. اختبِر لون الخلفية. انتبه للعينين وتجنب الألوان الزاهية أو المتوهجة للغاية.

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

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

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