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

يجب أن يفصح تطبيقك عن MediaBrowserService باستخدام فلتر intent في البيان. يمكنك اختيار اسم خدمتك الخاصة. في المثال التالي، هي "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() معرِّف 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) يحدد الاتصالات المسموح بها، أو يحصي بدلاً من ذلك الاتصالات التي يجب حظرها. للحصول على مثال حول كيفية تنفيذ قائمة التحكم بالوصول (ACL) تسمح باتصالات محددة، يمكنك الاطلاع على PackageValidator (أداة التحقّق من صحة الحزمة) في برنامج 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() وظيفة تحويل تحويل حالة تشغيل PendingState إلى رمز PendingIntent.