פיתוח שירות לדפדפן מדיה

האפליקציה שלך צריכה להצהיר על 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.

אתחול הסשן של המדיה

כשהשירות מקבל את שיטת הקריאה החוזרת (callback) של מחזור החיים של onCreate(), הוא צריך לבצע את השלבים הבאים:

  • יצירה ואתחול של סשן המדיה
  • הגדרת הקריאה החוזרת (callback) של סשן המדיה
  • הגדרת האסימון של סשן המדיה

הקוד 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()

ה-method onGetRoot() מחזירה את הצומת הבסיסי (root) של היררכיית התוכן. אם ה-method מחזירה null, החיבור נדחה.

כדי לאפשר ללקוחות להתחבר לשירות שלכם ולעיין בתוכן המדיה שלו, onGetRoot() צריך להחזיר דפדפן BrowserRoot שאינו אפס, שהוא מזהה שורש שמייצג את היררכיית התוכן שלכם.

כדי לאפשר ללקוחות להתחבר ל-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) שמציין אילו חיבורים מותרים, או לחלופין אילו חיבורים אסורים. דוגמה לאופן ההטמעה של ACL שמאפשרת חיבורים ספציפיים, PackageValidator ב-Universal Android Music Player אפליקציה לדוגמה.

מומלץ לספק היררכיות תוכן שונות בהתאם ל: איזה סוג של לקוח שולח את השאילתה. באופן ספציפי, מערכת Android Auto מגבילה שמשתמשים יוצרים אינטראקציה עם אפליקציות אודיו. למידע נוסף, ראו השמעת אודיו עבור אוטומטי. שלך יכול לבדוק את clientPackageName בזמן החיבור כדי לקבוע סוג, ומחזירה ערך שונה של BrowserRoot בהתאם ללקוח (או rootHints) אם בכלל).

תקשורת תוכן עם onLoadChildren()

לאחר שהלקוח יתחבר, הוא יכול לעבור את היררכיית התוכן על ידי ביצוע קריאות חוזרות אל MediaBrowserCompat.subscribe() כדי לבנות ייצוג מקומי של ממשק המשתמש. ה-method 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 שלו).
  • הקריאה החוזרת (callback) של onPlay() בסשן המדיה צריכה לכלול קוד שקורא ל-startService(). הדבר מבטיח שהשירות יתחיל וימשיך לפעול, גם כאשר כל פעילויות MediaBrowser שבממשק המשתמש שמקושרות אליו יבוטלו.
  • הקריאה החוזרת (callback) של onStop() צריכה להתקשר ל-stopSelf(). אם השירות הופעל, הוא יופסק. בנוסף, השירות מושמד אם לא משויכות אליו פעילויות. אחרת, השירות יישאר מקושר עד לביטול הקישור של כל הפעילויות שלו. (אם תתקבל קריאה נוספת מסוג startService() לפני שהשירות יושמד, העצירה שבהמתנה תבוטל.)

תרשים הזרימה הבא מדגים את ניהול מחזור החיים של שירות. מונה המשתנים עוקב אחר מספר הלקוחות המקושרים:

מחזור חיים של שירות

שימוש בהתראות MediaStyle עם שירות שפועל בחזית

כשהשירות פועל, הוא צריך לפעול בחזית. הדבר מאפשר למערכת לדעת שהשירות ממלא פונקציה מועילה ואין להשבית אותו אם אין מספיק זיכרון במערכת. שירות שפועל בחזית חייב להציג התראה כדי שהמשתמשים ידעו עליו ויוכלו לשלוט בו. הקריאה החוזרת (callback) של 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 בדרך.
  • חשוב להיזהר כשמגדירים את צבע הרקע. בהודעה רגילה במסגרת ב-Android מגרסה 5.0 ואילך, הצבע חל רק על הרקע של סמל אפליקציה קטן. לעומת זאת, עבור התראות MediaStyle שקודמות ל-Android 7.0, הצבע משמש לרקע המלא של ההתראה. בודקים את צבע הרקע. התחל עדינים לעיניים ונמנעים מצבעים בהירים או פלואורסצנטיים במיוחד.

ההגדרות האלה זמינות רק כשמשתמשים ב-NotificationCompat.MediaStyle:

  • שימוש בפורמט setMediaSession() כדי לשייך את ההתראה לסשן שלכם. ההרשאה הזו מאפשרת לאפליקציות צד שלישי ומכשירים נלווים כדי לגשת לסשן ולשלוט בו.
  • אפשר להוסיף עד 3 פעולות שיופיעו באמצעות setShowActionsInCompactView() ContentView בגודל רגיל של ההתראה. (כאן לחצן ההשהיה .)
  • ב-Android 5.0 (רמת API 21) ואילך ניתן להחליק החוצה התראה כדי להפסיק את הנגן ברגע שהשירות כבר לא פועל בחזית. אי אפשר לבצע את זה בגרסאות קודמות. כדי לאפשר למשתמשים להסיר את ההתראה ולהפסיק את ההפעלה לפני Android 5.0 (רמת API 21), יש לך אפשרות להוסיף לחצן ביטול בפינה השמאלית העליונה של התראה באמצעות חיוג אל setShowCancelButton(true) ואל setCancelButtonIntent().

כשמוסיפים את לחצני ההשהיה והביטול, צריך PendingIntent כדי לצרף לפעולת ההפעלה. השיטה MediaButtonReceiver.buildMediaButtonPendingIntent() מבצעת את פעולת ההמרה פעולת PlayState ל-PendingIntent.