Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bcecf31a authored by Jaewan Kim's avatar Jaewan Kim
Browse files

MediaSession2: Initial commit of MediaLibraryService2

MediaLibraryService2 is the new name for the MediaBrowserService

Test: Run all MediaComponents tests once
Change-Id: I7a1ae20ff59aa4714cff08e43cdabb5b8c557b98
parent ec877287
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ interface IMediaSession2 {
    PlaybackState getPlaybackState();

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Get browser service specific
    // Get library service specific
    //////////////////////////////////////////////////////////////////////////////////////////////
    oneway void getBrowserRoot(IMediaSession2Callback callback, in Bundle rootHints);

+5 −0
Original line number Diff line number Diff line
@@ -44,4 +44,9 @@ oneway interface IMediaSession2Callback {
    //               Follow-up TODO: Add similar functions to the session.
    // TODO(jaewan): Is term 'accepted/rejected' correct? For permission, 'grant' is used.
    void onConnectionChanged(IMediaSession2 sessionBinder, in Bundle commandGroup);

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Browser sepcific
    //////////////////////////////////////////////////////////////////////////////////////////////
    void onGetRootResult(in Bundle rootHints, String rootMediaId, in Bundle rootExtra);
}
+144 −0
Original line number Diff line number Diff line
/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.media.MediaSession2.BuilderBase;
import android.media.MediaSession2.ControllerInfo;
import android.media.update.ApiLoader;
import android.media.update.MediaSessionService2Provider;
import android.os.Bundle;
import android.service.media.MediaBrowserService.BrowserRoot;

/**
 * Base class for media library services.
 * <p>
 * Media library services enable applications to browse media content provided by an application
 * and ask the application to start playing it. They may also be used to control content that
 * is already playing by way of a {@link MediaSession2}.
 * <p>
 * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
 * <pre>
 * &lt;service android:name="component_name_of_your_implementation" &gt;
 *   &lt;intent-filter&gt;
 *     &lt;action android:name="android.media.MediaLibraryService2" /&gt;
 *   &lt;/intent-filter&gt;
 * &lt;/service&gt;</pre>
 * <p>
 * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't
 * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
 * default, an empty string will be used for ID of the service. If you want to specify an ID,
 * declare metadata in the manifest as follows.
 * @hide
 */
// TODO(jaewan): Unhide
public abstract class MediaLibraryService2 extends MediaSessionService2 {
    /**
     * This is the interface name that a service implementing a session service should say that it
     * support -- that is, this is the action it uses for its intent filter.
     */
    public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";

    /**
     * Session for the media library service.
     */
    public class MediaLibrarySession extends MediaSession2 {
        MediaLibrarySession(Context context, MediaPlayerBase player, String id,
                SessionCallback callback) {
            super(context, player, id, callback);
        }
        // TODO(jaewan): Place public methods here.
    }

    public static abstract class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
        /**
         * Called to get the root information for browsing by a particular client.
         * <p>
         * The implementation should verify that the client package has permission
         * to access browse media information before returning the root id; it
         * should return null if the client is not allowed to access this
         * information.
         *
         * @param controllerInfo information of the controller requesting access to browse media.
         * @param rootHints An optional bundle of service-specific arguments to send
         * to the media browser service when connecting and retrieving the
         * root id for browsing, or null if none. The contents of this
         * bundle may affect the information returned when browsing.
         * @return The {@link BrowserRoot} for accessing this app's content or null.
         * @see BrowserRoot#EXTRA_RECENT
         * @see BrowserRoot#EXTRA_OFFLINE
         * @see BrowserRoot#EXTRA_SUGGESTED
         */
        public abstract @Nullable BrowserRoot onGetRoot(
                @NonNull ControllerInfo controllerInfo, @Nullable Bundle rootHints);
    }

    /**
     * Builder for {@link MediaLibrarySession}.
     */
    // TODO(jaewan): Move this to updatable.
    public class MediaLibrarySessionBuilder
            extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
        public MediaLibrarySessionBuilder(
                @NonNull Context context, @NonNull MediaPlayerBase player,
                @NonNull MediaLibrarySessionCallback callback) {
            super(context, player);
            setSessionCallback(callback);
        }

        @Override
        public MediaLibrarySessionBuilder setSessionCallback(
                @NonNull MediaLibrarySessionCallback callback) {
            if (callback == null) {
                throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
            }
            return super.setSessionCallback(callback);
        }

        @Override
        public MediaLibrarySession build() throws IllegalStateException {
            return new MediaLibrarySession(mContext, mPlayer, mId, mCallback);
        }
    }

    @Override
    MediaSessionService2Provider createProvider() {
        return ApiLoader.getProvider(this).createMediaLibraryService2(this);
    }

    /**
     * Called when another app requested to start this service.
     * <p>
     * Library service will accept or reject the connection with the
     * {@link MediaLibrarySessionCallback} in the created session.
     * <p>
     * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
     * expected ID that you've specified through the AndroidManifest.xml.
     * <p>
     * This method will be called on the main thread.
     *
     * @param sessionId session id written in the AndroidManifest.xml.
     * @return a new browser session
     * @see MediaLibrarySessionBuilder
     * @see #getSession()
     * @throws RuntimeException if returned session is invalid
     */
    @Override
    public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
}
+43 −34
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package android.media;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,12 +28,9 @@ import android.media.update.MediaSession2Provider.ControllerInfoProvider;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.Process;
import android.text.TextUtils;
import android.util.ArraySet;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

@@ -55,20 +51,17 @@ import java.util.List;
 * instead. With it, your playback can be revived even after you've finished playback. See
 * {@link MediaSessionService2} for details.
 * <p>
 * A session can be obtained by {@link #getInstance(Context, Handler)}. The owner of the session may
 * pass its session token to other processes to allow them to create a {@link MediaController2}
 * to interact with the session.
 * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
 * to other processes to allow them to create a {@link MediaController2} to interact with the
 * session.
 * <p>
 * To receive transport control commands, an underlying media player must be set with
 * {@link #setPlayer(MediaPlayerBase)}. Commands will be sent to the underlying player directly
 * on the thread that had been specified by {@link #getInstance(Context, Handler)}.
 * When a session receive transport control commands, the session sends the commands directly to
 * the the underlying media player set by {@link Builder} or {@link #setPlayer(MediaPlayerBase)}.
 * <p>
 * When an app is finished performing playback it must call
 * {@link #setPlayer(MediaPlayerBase)} with {@code null} to clean up the session and notify any
 * controllers. It's developers responsibility of cleaning the session and releasing resources.
 * When an app is finished performing playback it must call {@link #close()} to clean up the session
 * and notify any controllers.
 * <p>
 * MediaSession2 objects should be used on the handler's thread that is initially given by
 * {@link #getInstance(Context, Handler)}.
 * {@link MediaSession2} objects should be used on the thread on the looper.
 *
 * @see MediaSessionService2
 * @hide
@@ -81,7 +74,7 @@ import java.util.List;
// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
//               developers that doesn't want to override from Browser, but user may not use this
//               correctly.
public final class MediaSession2 extends MediaPlayerBase {
public class MediaSession2 extends MediaPlayerBase {
    private final MediaSession2Provider mProvider;

    // Note: Do not define IntDef because subclass can add more command code on top of these.
@@ -321,19 +314,16 @@ public final class MediaSession2 extends MediaPlayerBase {
    };

    /**
     * Builder for {@link MediaSession2}.
     * <p>
     * Any incoming event from the {@link MediaController2} will be handled on the thread
     * that created session with the {@link Builder#build()}.
     * Base builder class for MediaSession2 and its subclass.
     *
     * @hide
     */
    // TODO(jaewan): Move this to updatable
    // TODO(jaewan): Add setRatingType()
    // TODO(jaewan): Add setSessionActivity()
    public final static class Builder {
        private final Context mContext;
        private final MediaPlayerBase mPlayer;
        private String mId;
        private SessionCallback mCallback;
    static abstract class BuilderBase
            <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
        final Context mContext;
        final MediaPlayerBase mPlayer;
        String mId;
        C mCallback;

        /**
         * Constructor.
@@ -343,7 +333,8 @@ public final class MediaSession2 extends MediaPlayerBase {
         * @throws IllegalArgumentException if any parameter is null, or the player is a
         *      {@link MediaSession2} or {@link MediaController2}.
         */
        public Builder(@NonNull Context context, @NonNull MediaPlayerBase player) {
        // TODO(jaewan): Also need executor
        public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) {
            if (context == null) {
                throw new IllegalArgumentException("context shouldn't be null");
            }
@@ -370,12 +361,12 @@ public final class MediaSession2 extends MediaPlayerBase {
         * @throws IllegalArgumentException if id is {@code null}
         * @return
         */
        public Builder setId(@NonNull String id) {
        public T setId(@NonNull String id) {
            if (id == null) {
                throw new IllegalArgumentException("id shouldn't be null");
            }
            mId = id;
            return this;
            return (T) this;
        }

        /**
@@ -384,9 +375,9 @@ public final class MediaSession2 extends MediaPlayerBase {
         * @param callback session callback.
         * @return
         */
        public Builder setSessionCallback(@Nullable SessionCallback callback) {
        public T setSessionCallback(@Nullable C callback) {
            mCallback = callback;
            return this;
            return (T) this;
        }

        /**
@@ -396,6 +387,24 @@ public final class MediaSession2 extends MediaPlayerBase {
         * @throws IllegalStateException if the session with the same id is already exists for the
         *      package.
         */
        public abstract MediaSession2 build() throws IllegalStateException;
    }

    /**
     * Builder for {@link MediaSession2}.
     * <p>
     * Any incoming event from the {@link MediaController2} will be handled on the thread
     * that created session with the {@link Builder#build()}.
     */
    // TODO(jaewan): Move this to updatable
    // TODO(jaewan): Add setRatingType()
    // TODO(jaewan): Add setSessionActivity()
    public static final class Builder extends BuilderBase<Builder, SessionCallback> {
        public Builder(Context context, @NonNull MediaPlayerBase player) {
            super(context, player);
        }

        @Override
        public MediaSession2 build() throws IllegalStateException {
            if (mCallback == null) {
                mCallback = new SessionCallback();
@@ -492,7 +501,7 @@ public final class MediaSession2 extends MediaPlayerBase {
     *       framework had to add heuristics to figure out if an app is
     * @hide
     */
    private MediaSession2(Context context, MediaPlayerBase player, String id,
    MediaSession2(Context context, MediaPlayerBase player, String id,
            SessionCallback callback) {
        super();
        mProvider = ApiLoader.getProvider(context)
+13 −30
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ import android.media.update.MediaSessionService2Provider;
import android.os.IBinder;

/**
 * Service version of the {@link MediaSession2}.
 * Base class for media session services, which is the service version of the {@link MediaSession2}.
 * <p>
 * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
 * to keep media playback in the background.
@@ -43,11 +43,11 @@ import android.os.IBinder;
 * </ul>
 * For example, user's voice command can start playback of your app even when it's not running.
 * <p>
 * To use this class, adding followings directly to your {@code AndroidManifest.xml}.
 * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
 * <pre>
 * &lt;service android:name="component_name_of_your_implementation" &gt;
 *   &lt;intent-filter&gt;
 *     &lt;action android:name="android.media.session.MediaSessionService2" /&gt;
 *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
 *   &lt;/intent-filter&gt;
 * &lt;/service&gt;</pre>
 * <p>
@@ -58,7 +58,7 @@ import android.os.IBinder;
 * <pre>
 * &lt;service android:name="component_name_of_your_implementation" &gt;
 *   &lt;intent-filter&gt;
 *     &lt;action android:name="android.media.session.MediaSessionService2" /&gt;
 *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
 *   &lt;/intent-filter&gt;
 *   &lt;meta-data android:name="android.media.session"
 *       android:value="session_id"/&gt;
@@ -120,8 +120,7 @@ public abstract class MediaSessionService2 extends Service {
     * This is the interface name that a service implementing a session service should say that it
     * support -- that is, this is the action it uses for its intent filter.
     */
    public static final String SERVICE_INTERFACE =
            "android.media.session.MediaSessionService2";
    public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";

    /**
     * Name under which a MediaSessionService2 component publishes information about itself.
@@ -129,21 +128,13 @@ public abstract class MediaSessionService2 extends Service {
     */
    public static final String SERVICE_META_DATA = "android.media.session";

    /**
     * Default notification channel ID used by {@link #onUpdateNotification(PlaybackState)}.
     *
     */
    public static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";

    /**
     * Default notification channel ID used by {@link #onUpdateNotification(PlaybackState)}.
     *
     */
    public static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;

    public MediaSessionService2() {
        super();
        mProvider = ApiLoader.getProvider(this).createMediaSessionService2(this);
        mProvider = createProvider();
    }

    MediaSessionService2Provider createProvider() {
        return ApiLoader.getProvider(this).createMediaSessionService2(this);
    }

    /**
@@ -168,32 +159,24 @@ public abstract class MediaSessionService2 extends Service {
     * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
     * expected ID that you've specified through the AndroidManifest.xml.
     * <p>
     * This method will be call on the main thread.
     * This method will be called on the main thread.
     *
     * @param sessionId session id written in the AndroidManifest.xml.
     * @return a new session
     * @see MediaSession2.Builder
     * @see #getSession()
     */
    // TODO(jaewan): Replace this with onCreateSession(). Its sesssion callback will replace
    //               this abstract method.
    // TODO(jaewan): Should we also include/documents notification listener access?
    // TODO(jaewan): Is term accepted/rejected correct? For permission, granted is more common.
    // TODO(jaewan): Return ConnectResult() that encapsulate supported action and player.
    public @NonNull abstract MediaSession2 onCreateSession(String sessionId);

    /**
     * Called when the playback state of this session is changed, and notification needs update.
     * <p>
     * Default media style notification will be shown if you don't override this or return
     * {@code null}. Override this method to show your own notification UI.
     * Override this method to show your own notification UI.
     * <p>
     * With the notification returned here, the service become foreground service when the playback
     * is started. It becomes background service after the playback is stopped.
     *
     * @param state playback state
     * @return a {@link MediaNotification}. If it's {@code null}, default notification will be shown
     *     instead.
     * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
     */
    // TODO(jaewan): Also add metadata
    public MediaNotification onUpdateNotification(PlaybackState state) {
Loading