يجب أن يذكر تطبيقك 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.