RemoteControlClientCompat.java

/*
 * Copyright (C) 2013 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 androidx.mediarouter.media;

import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.lang.ref.WeakReference;

/**
 * Provides access to features of the remote control client.
 *
 * Hidden for now but we might want to make this available to applications
 * in the future.
 */
abstract class RemoteControlClientCompat {
    protected final Context mContext;
    protected final android.media.RemoteControlClient mRcc;
    protected VolumeCallback mVolumeCallback;

    protected RemoteControlClientCompat(Context context, android.media.RemoteControlClient rcc) {
        mContext = context;
        mRcc = rcc;
    }

    public static RemoteControlClientCompat obtain(
            Context context, android.media.RemoteControlClient rcc) {
        return new JellybeanImpl(context, rcc);
    }

    public android.media.RemoteControlClient getRemoteControlClient() {
        return mRcc;
    }

    /**
     * Sets the current playback information.
     * Must be called at least once to attach to the remote control client.
     *
     * @param info The playback information.  Must not be null.
     */
    public void setPlaybackInfo(PlaybackInfo info) {
    }

    /**
     * Sets a callback to receive volume change requests from the remote control client.
     *
     * @param callback The volume callback to use or null if none.
     */
    public void setVolumeCallback(VolumeCallback callback) {
        mVolumeCallback = callback;
    }

    /**
     * Specifies information about the playback.
     */
    public static final class PlaybackInfo {
        public int volume;
        public int volumeMax;
        public int volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
        public int playbackStream = AudioManager.STREAM_MUSIC;
        public int playbackType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
        @Nullable
        public String volumeControlId;
    }

    /**
     * Called when volume updates are requested by the remote control client.
     */
    public interface VolumeCallback {
        /**
         * Called when the volume should be increased or decreased.
         *
         * @param direction An integer indicating whether the volume is to be increased
         *                  (positive value) or decreased (negative value).
         *                  For bundled changes, the absolute value indicates the number of changes
         *                  in the same direction, e.g. +3 corresponds to three "volume up" changes.
         */
        void onVolumeUpdateRequest(int direction);

        /**
         * Called when the volume for the route should be set to the given value.
         *
         * @param volume An integer indicating the new volume value that should be used,
         *               always between 0 and the value set by {@link PlaybackInfo#volumeMax}.
         */
        void onVolumeSetRequest(int volume);
    }

    /**
     * Implementation for Jellybean.
     *
     * The basic idea of this implementation is to attach the RCC to a UserRouteInfo
     * in order to hook up stream metadata and volume callbacks because there is no
     * other API available to do so in this platform version.  The UserRouteInfo itself
     * is not attached to the MediaRouter so it is transparent to the user.
     */
    static class JellybeanImpl extends RemoteControlClientCompat {
        private final android.media.MediaRouter mRouter;
        private final android.media.MediaRouter.RouteCategory mUserRouteCategory;
        private final android.media.MediaRouter.UserRouteInfo mUserRoute;
        private boolean mRegistered;

        JellybeanImpl(Context context, android.media.RemoteControlClient rcc) {
            super(context, rcc);

            mRouter =
                    (android.media.MediaRouter)
                            context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
            mUserRouteCategory =
                    mRouter.createRouteCategory(/* name= */ "", /* isGroupable= */ false);
            mUserRoute = mRouter.createUserRoute(mUserRouteCategory);
        }

        @SuppressLint("WrongConstant") // False positive. See b/310913043.
        @Override
        public void setPlaybackInfo(PlaybackInfo info) {
            mUserRoute.setVolume(info.volume);
            mUserRoute.setVolumeMax(info.volumeMax);
            mUserRoute.setVolumeHandling(info.volumeHandling);
            mUserRoute.setPlaybackStream(info.playbackStream);
            mUserRoute.setPlaybackType(info.playbackType);

            if (!mRegistered) {
                mRegistered = true;
                mUserRoute.setVolumeCallback(
                        MediaRouterUtils.createVolumeCallback(new VolumeCallbackWrapper(this)));
                mUserRoute.setRemoteControlClient(mRcc);
            }
        }

        private static final class VolumeCallbackWrapper
                implements MediaRouterUtils.VolumeCallback {
            // Unfortunately, the framework never unregisters its volume observer from
            // the audio service so the UserRouteInfo object may leak along with
            // any callbacks that we attach to it.  Use a weak reference to prevent
            // the volume callback from holding strong references to anything important.
            private final WeakReference<JellybeanImpl> mImplWeak;

            public VolumeCallbackWrapper(JellybeanImpl impl) {
                mImplWeak = new WeakReference<>(impl);
            }

            @Override
            public void onVolumeUpdateRequest(@NonNull android.media.MediaRouter.RouteInfo route,
                    int direction) {
                JellybeanImpl impl = mImplWeak.get();
                if (impl != null && impl.mVolumeCallback != null) {
                    impl.mVolumeCallback.onVolumeUpdateRequest(direction);
                }
            }

            @Override
            public void onVolumeSetRequest(@NonNull android.media.MediaRouter.RouteInfo route,
                    int volume) {
                JellybeanImpl impl = mImplWeak.get();
                if (impl != null && impl.mVolumeCallback != null) {
                    impl.mVolumeCallback.onVolumeSetRequest(volume);
                }
            }
        }
    }
}