درحال ساختن سرویس مرورگر رسانه

برنامه شما باید MediaBrowserService را با فیلتر قصد در مانیفست خود اعلام کند. می‌توانید نام سرویس خودتان را انتخاب کنید. در مثال زیر، نام سرویس انتخاب‌شده MediaPlaybackService است.

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

مقداردهی اولیه جلسه رسانه

وقتی سرویس روش فراخوانی چرخه حیات onCreate() را دریافت می‌کند، باید این مراحل را انجام دهد:

کد onCreate() زیر این مراحل را نشان می‌دهد:

کاتلین

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

جاوا

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() گره ریشه سلسله‌مراتب محتوا را برمی‌گرداند. اگر روش null را برگرداند، اتصال رد می‌شود.

برای اینکه مشتریان بتوانند به سرویس شما متصل شوند و محتوای رسانه‌ای آن را مرور کنند، onGetRoot() باید BrowserRoot غیرتهی برگرداند که شناسه ریشه‌ای است که سلسله‌مراتب محتوای شما را نشان می‌دهد.

برای اینکه کارخواه‌ها بتوانند بدون مرور کردن به MediaSession شما متصل شوند، onGetRoot() باید همچنان BrowserRoot غیرتهی برگرداند، اما شناسه ریشه باید نشان‌دهنده سلسله‌مراتب محتوای خالی باشد.

پیاده‌سازی معمول onGetRoot() ممکن است به این شکل باشد:

کاتلین

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

جاوا

@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 نحوه تعامل کاربران با برنامه‌های صوتی را محدود می‌کند. برای اطلاعات بیشتر، به پخش صدا برای Auto مراجعه کنید. می‌توانید clientPackageName را در زمان اتصال بررسی کنید تا نوع کارخواه را تعیین کنید، و بسته به کارخواه (یا rootHints درصورت وجود)، BrowserRoot متفاوتی را برگردانید.

درحال انتقال محتوا با onLoadChildren()

پس‌از اتصال کارخواه، می‌تواند با برقراری تماس‌های مکرر با MediaBrowserCompat.subscribe()، سلسله‌مراتب محتوا را پیمایش کند تا نمایشی محلی از واسط کاربر بسازد. روش subscribe() فراخوانی برگشتی onLoadChildren() را به سرویس ارسال می‌کند، که فهرستی از اشیاء MediaBrowser.MediaItem را برمی‌گرداند.

هر MediaItem رشته شناسه یکتایی دارد که یک توکن مبهم است. وقتی کارخواه می‌خواهد زیرمنویی را باز کند یا موردی را پخش کند، شناسه را ارسال می‌کند. سرویس شما مسئول مرتبط کردن شناسه با گره منو یا مورد محتوای مناسب است.

پیاده‌سازی ساده onLoadChildren() ممکن است به این شکل باشد:

کاتلین

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

جاوا

@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 نباید حاوی نقشه بیتی نماد باشند. هنگام ساختن MediaDescription برای هر مورد، به‌جای آن از Uri با فراخوانی setIconUri() استفاده کنید.

برای نمونه‌ای از نحوه پیاده‌سازی onLoadChildren()، برنامه نمونه پخش‌کننده موسیقی Android جهانی را ببینید.

چرخه حیات سرویس مرورگر رسانه

رفتار سرویس 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() به شما امکان می‌دهد کنترل‌کننده رسانه را مستقیماً از جلسه رسانه خود ایجاد کنید.

کاتلین

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

جاوا

// 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 بهترین گزینه است.
  • هنگام تنظیم رنگ پس‌زمینه مراقب باشید. در اعلان معمولی در Android نسخه ۵.۰ یا بالاتر، رنگ فقط به پس‌زمینه نماد کوچک برنامه اعمال می‌شود. اما برای اعلان‌های MediaStyle قبل‌از Android 7.0، از رنگ برای کل پس‌زمینه اعلان استفاده می‌شود. رنگ پس‌زمینه را آزمایش کنید. از رنگ‌های ملایم استفاده کنید و از رنگ‌های بسیار روشن یا فلورسنت اجتناب کنید.

این تنظیمات فقط زمانی دردسترس است که از NotificationCompat.MediaStyle استفاده می‌کنید:

  • از setMediaSession() برای مرتبط کردن اعلان با جلسه‌تان استفاده کنید. این کار به برنامه‌های طرف سوم و دستگاه‌های همپا اجازه می‌دهد به جلسه دسترسی داشته باشند و آن را کنترل کنند.
  • از setShowActionsInCompactView() برای افزودن حداکثر ۳ کنش برای نمایش در نمای محتوای اندازه استاندارد اعلان استفاده کنید. (در اینجا دکمه مکث مشخص شده است.)
  • در Android 5.0 (سطح میانای برنامه‌سازی کاربردی ۲۱) و نسخه‌های جدیدتر، وقتی سرویس دیگر در پیش‌زمینه اجرا نمی‌شود، می‌توانید با تند کشیدن اعلان، پخش‌کننده را متوقف کنید. در نسخه‌های قبلی نمی‌توانید این کار را انجام دهید. برای اینکه کاربران بتوانند اعلان را بردارند و بازپخش را متوقف کنند، قبل‌از Android 5.0 (سطح API‏ ۲۱)، می‌توانید با فراخوانی setShowCancelButton(true) و setCancelButtonIntent()، دکمه لغو را در گوشه بالا سمت راست اعلان اضافه کنید.

وقتی دکمه‌های توقف موقت و لغو را اضافه می‌کنید، به PendingIntent برای پیوست کردن به کنش بازپخش نیاز دارید. روش MediaButtonReceiver.buildMediaButtonPendingIntent() وظیفه تبدیل کنش PlaybackState به PendingIntent را انجام می‌دهد.

فعال کردن مرور رسانه AVRCP

علاوه‌بر برنامه‌های سفارشی مانند Android Auto، لایه بلوتوث سیستم نیز به‌عنوان کارخواه برای MediaBrowserService شما عمل می‌کند تا مرور کاتالوگ از راه دور بی‌سیم (AVRCP) را تسهیل کند.

در Android 16 و Android 17، پلاتفرم از برنامه‌هایی که از Media3 استفاده نمی‌کنند می‌خواهد فعالیت خاصی را با فیلتر هدف برای اعتبارسنجی مرور نشان دهند.

این فیلتر هدف خاص را به فعالیت صادرشده در 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>