ساخت یک سرویس مرورگر رسانه

برنامه شما باید MediaBrowserService را با یک intent-filter در فایل manifest خود تعریف کند. شما می‌توانید نام سرویس خود را انتخاب کنید. در مثال زیر، نام سرویس انتخاب شده 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 غیر تهی (non-null) برگرداند که یک شناسه ریشه (root ID) است که نشان دهنده سلسله مراتب محتوای شما می‌باشد.

برای اینکه کلاینت‌ها بتوانند بدون مرور وب به 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 نحوه تعامل کاربران با برنامه‌های صوتی را محدود می‌کند. برای اطلاعات بیشتر، به بخش «پخش صدا برای خودکار» مراجعه کنید. می‌توانید در زمان اتصال به 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 برای هر آیتم، با فراخوانی تابع setIconUri() از یک Uri استفاده کنید.

برای مثالی از نحوه‌ی پیاده‌سازی onLoadChildren() ، به برنامه‌ی نمونه‌ی Universal Android Music Player مراجعه کنید.

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

رفتار یک سرویس اندروید بستگی به این دارد که آیا شروع شده است یا به یک یا چند کلاینت متصل است . پس از ایجاد یک سرویس، می‌توان آن را شروع کرد، متصل کرد یا هر دو. در تمام این حالت‌ها، کاملاً کاربردی است و می‌تواند کاری را که برای آن طراحی شده است انجام دهد. تفاوت در مدت زمانی است که سرویس وجود خواهد داشت. یک سرویس متصل تا زمانی که همه کلاینت‌های متصل آن از حالت اتصال خارج نشوند، از بین نمی‌رود. یک سرویس شروع شده را می‌توان به صراحت متوقف و از بین برد (با فرض اینکه دیگر به هیچ کلاینتی متصل نیست).

وقتی یک MediaBrowser که در یک اکتیویتی دیگر اجرا می‌شود به یک MediaBrowserService متصل می‌شود، اکتیویتی را به سرویس متصل می‌کند و سرویس را مقید (اما شروع نشده) می‌سازد. این رفتار پیش‌فرض در کلاس MediaBrowserServiceCompat تعبیه شده است.

سرویسی که فقط متصل است (و شروع نشده است) وقتی همه کلاینت‌هایش از حالت اتصال خارج شوند، از بین می‌رود. اگر فعالیت رابط کاربری شما در این مرحله قطع شود، سرویس از بین می‌رود. اگر هنوز هیچ موسیقی پخش نکرده باشید، این مشکلی ایجاد نمی‌کند. با این حال، وقتی پخش شروع می‌شود، کاربر احتمالاً انتظار دارد که حتی پس از تعویض برنامه‌ها، به گوش دادن ادامه دهد. شما نمی‌خواهید وقتی رابط کاربری را برای کار با برنامه دیگری از حالت اتصال خارج می‌کنید، پخش‌کننده از بین برود.

به همین دلیل، باید با فراخوانی startService() مطمئن شوید که سرویس هنگام شروع پخش، آغاز شده است. یک سرویس آغاز شده باید صریحاً متوقف شود، چه محدود شده باشد و چه نباشد. این تضمین می‌کند که پخش‌کننده شما حتی اگر فعالیت رابط کاربری کنترل‌کننده از حالت اتصال خارج شود، به اجرای خود ادامه می‌دهد.

برای متوقف کردن یک سرویس شروع شده، Context.stopService() یا stopSelf() را فراخوانی کنید. سیستم در اسرع وقت سرویس را متوقف و از بین می‌برد. با این حال، اگر یک یا چند کلاینت هنوز به سرویس متصل باشند، فراخوانی برای متوقف کردن سرویس تا زمانی که همه کلاینت‌های آن از حالت اتصال خارج شوند، به تأخیر می‌افتد.

چرخه حیات MediaBrowserService توسط نحوه ایجاد آن، تعداد کلاینت‌هایی که به آن متصل هستند و فراخوانی‌هایی که از فراخوانی‌های مجدد جلسه رسانه دریافت می‌کند، کنترل می‌شود. به طور خلاصه:

  • این سرویس زمانی ایجاد می‌شود که در پاسخ به یک دکمه رسانه یا زمانی که یک فعالیت به آن متصل می‌شود (پس از اتصال از طریق MediaBrowser ) آغاز به کار کند.
  • تابع فراخوانی onPlay() در session رسانه باید شامل کدی باشد که startService() را فراخوانی می‌کند. این تضمین می‌کند که سرویس شروع به کار می‌کند و به اجرای خود ادامه می‌دهد، حتی زمانی که تمام فعالیت‌های UI MediaBrowser که به آن متصل هستند، از حالت اتصال خارج شوند.
  • تابع فراخوانی onStop() باید تابع stopSelf() را فراخوانی کند. اگر سرویس شروع شده باشد، آن را متوقف می‌کند. علاوه بر این، اگر هیچ فعالیتی به آن متصل نباشد، سرویس از بین می‌رود. در غیر این صورت، سرویس تا زمانی که تمام فعالیت‌هایش از حالت اتصال خارج شوند، متصل باقی می‌ماند. (اگر قبل از از بین رفتن سرویس، فراخوانی startService() بعدی دریافت شود، توقف در حال انتظار لغو می‌شود.)

فلوچارت زیر نحوه مدیریت چرخه حیات یک سرویس را نشان می‌دهد. متغیر شمارنده تعداد کلاینت‌های متصل را ردیابی می‌کند:

Service Lifecycle

استفاده از اعلان‌های MediaStyle با یک سرویس پیش‌زمینه

وقتی یک سرویس در حال اجرا است، باید در پیش‌زمینه اجرا شود. این به سیستم اطلاع می‌دهد که سرویس در حال انجام یک عملکرد مفید است و در صورت کمبود حافظه سیستم، نباید از کار بیفتد. یک سرویس پیش‌زمینه باید یک اعلان نمایش دهد تا کاربر از آن مطلع شود و بتواند به صورت اختیاری آن را کنترل کند. تابع فراخوانی onPlay() باید سرویس را در پیش‌زمینه قرار دهد. (توجه داشته باشید که این معنای خاصی از "پیش‌زمینه" است. در حالی که اندروید سرویس را در پیش‌زمینه برای اهداف مدیریت فرآیند در نظر می‌گیرد، برای کاربر، پخش‌کننده در پس‌زمینه در حال پخش است در حالی که برنامه دیگری در "پیش‌زمینه" روی صفحه قابل مشاهده است.)

وقتی یک سرویس در پیش‌زمینه اجرا می‌شود، باید یک اعلان نمایش دهد، که در حالت ایده‌آل شامل یک یا چند کنترل انتقال است. این اعلان همچنین باید شامل اطلاعات مفیدی از فراداده‌های جلسه باشد.

اعلان را بسازید و هنگام شروع پخش پخش کنید. بهترین مکان برای انجام این کار درون متد MediaSessionCompat.Callback.onPlay() است.

مثال زیر از NotificationCompat.MediaStyle استفاده می‌کند که برای برنامه‌های رسانه‌ای طراحی شده است. این مثال نحوه ساخت یک اعلان (notification) را نشان می‌دهد که متادیتا و کنترل‌های انتقال را نمایش می‌دهد. متد 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 راه حل مناسبی است.
  • هنگام تنظیم رنگ پس‌زمینه مراقب باشید. در یک اعلان معمولی در اندروید نسخه ۵.۰ یا بالاتر، رنگ فقط به پس‌زمینه آیکون کوچک برنامه اعمال می‌شود. اما برای اعلان‌های MediaStyle قبل از اندروید ۷.۰، از این رنگ برای کل پس‌زمینه اعلان استفاده می‌شود. رنگ پس‌زمینه خود را آزمایش کنید. با چشمان خود ملایم باشید و از رنگ‌های بسیار روشن یا فلورسنت خودداری کنید.

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

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

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

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

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

در اندروید ۱۶ و اندروید ۱۷، این پلتفرم ایجاب می‌کند که برنامه‌هایی که از Media3 استفاده نمی‌کنند، یک فعالیت خاص را با یک فیلتر intent برای اعتبارسنجی جهت مرور در معرض نمایش قرار دهند.

این فیلتر intent خاص را به یک activity اکسپورت شده در AndroidManifest.xml خود اضافه کنید. توجه داشته باشید که CATEGORY_DEFAULT عمداً حذف شده است تا از نمایش برنامه شما در منوهای عمومی "Open with" برای فایل‌های صوتی محلی جلوگیری شود:

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