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

Commit 30faa348 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Make RouteSessionController as inner class of MediaRouter2"

parents 9d645a43 8c4cf9e0
Loading
Loading
Loading
Loading
+174 −13
Original line number Original line Diff line number Diff line
@@ -291,13 +291,15 @@ public class MediaRouter2 {
     * @param executor the executor to get the result of the session creation
     * @param executor the executor to get the result of the session creation
     * @param callback the callback to get the result of the session creation
     * @param callback the callback to get the result of the session creation
     *
     *
     * @see SessionCreationCallback#onSessionCreated(RouteSessionController, Bundle)
     * @see SessionCallback#onSessionCreated(RouteSessionController, Bundle)
     * @see SessionCreationCallback#onSessionCreationFailed()
     * @see SessionCallback#onSessionCreationFailed()
     *
     * TODO: Separate callback registeration from creating session request.
     */
     */
    @NonNull
    @NonNull
    public void requestCreateSession(@NonNull MediaRoute2Info route,
    public void requestCreateSession(@NonNull MediaRoute2Info route,
            @NonNull String controlCategory,
            @NonNull String controlCategory,
            @CallbackExecutor Executor executor, @NonNull SessionCreationCallback callback) {
            @CallbackExecutor Executor executor, @NonNull SessionCallback callback) {
        Objects.requireNonNull(route, "route must not be null");
        Objects.requireNonNull(route, "route must not be null");
        if (TextUtils.isEmpty(controlCategory)) {
        if (TextUtils.isEmpty(controlCategory)) {
            throw new IllegalArgumentException("controlCategory must not be empty");
            throw new IllegalArgumentException("controlCategory must not be empty");
@@ -488,9 +490,9 @@ public class MediaRouter2 {
    }
    }


    /**
    /**
     * Creates a controller and calls the {@link SessionCreationCallback#onSessionCreated}.
     * Creates a controller and calls the {@link SessionCallback#onSessionCreated}.
     * If session creation has failed, then it calls
     * If session creation has failed, then it calls
     * {@link SessionCreationCallback#onSessionCreationFailed()}.
     * {@link SessionCallback#onSessionCreationFailed()}.
     * <p>
     * <p>
     * Pass {@code null} to sessionInfo for the failure case.
     * Pass {@code null} to sessionInfo for the failure case.
     */
     */
@@ -512,7 +514,7 @@ public class MediaRouter2 {
        mSessionCreationRequests.remove(matchingRequest);
        mSessionCreationRequests.remove(matchingRequest);


        final Executor executor = matchingRequest.mExecutor;
        final Executor executor = matchingRequest.mExecutor;
        final SessionCreationCallback callback = matchingRequest.mSessionCreationCallback;
        final SessionCallback callback = matchingRequest.mSessionCallback;


        if (sessionInfo == null) {
        if (sessionInfo == null) {
            // TODO: We may need to distinguish between failure and rejection.
            // TODO: We may need to distinguish between failure and rejection.
@@ -580,9 +582,9 @@ public class MediaRouter2 {
    }
    }


    /**
    /**
     * Callback for receiving a result of session creation.
     * Callback for receiving a result of session creation and session updates.
     */
     */
    public static class SessionCreationCallback {
    public static class SessionCallback {
        /**
        /**
         * Called when the route session is created by the route provider.
         * Called when the route session is created by the route provider.
         *
         *
@@ -594,6 +596,166 @@ public class MediaRouter2 {
         * Called when the session creation request failed.
         * Called when the session creation request failed.
         */
         */
        public void onSessionCreationFailed() {}
        public void onSessionCreationFailed() {}

        /**
         * Called when the session info has changed.
         */
        void onSessionInfoChanged(RouteSessionController controller, RouteSessionInfo newInfo,
                RouteSessionInfo oldInfo) {}

        /**
         * Called when the session is released. Session can be released by the controller using
         * {@link RouteSessionController#release(boolean)}, or by the
         * {@link MediaRoute2ProviderService} itself. One can do clean-ups here.
         *
         * TODO: When Provider#notifySessionDestroyed is introduced, add @see for the method.
         */
        void onSessionReleased(RouteSessionController controller, int reason, boolean shouldStop) {}
    }

    /**
     * A class to control media route session in media route provider.
     * For example, adding/removing/transferring routes to session can be done through this class.
     * Instances are created by {@link MediaRouter2}.
     *
     * @hide
     */
    public final class RouteSessionController {
        private final Object mLock = new Object();

        @GuardedBy("mLock")
        private RouteSessionInfo mSessionInfo;

        @GuardedBy("mLock")
        private volatile boolean mIsReleased;

        RouteSessionController(@NonNull RouteSessionInfo sessionInfo) {
            mSessionInfo = sessionInfo;
        }

        /**
         * @return the ID of this controller
         */
        public int getSessionId() {
            synchronized (mLock) {
                return mSessionInfo.getSessionId();
            }
        }

        /**
         * @return the category of routes that the session includes.
         */
        @NonNull
        public String getControlCategory() {
            synchronized (mLock) {
                return mSessionInfo.getControlCategory();
            }
        }

        /**
         * @return the control hints used to control route session if available.
         */
        @Nullable
        public Bundle getControlHints() {
            synchronized (mLock) {
                return mSessionInfo.getControlHints();
            }
        }

        /**
         * @return the unmodifiable list of IDs of currently selected routes
         */
        @NonNull
        public List<String> getSelectedRoutes() {
            synchronized (mLock) {
                return Collections.unmodifiableList(mSessionInfo.getSelectedRoutes());
            }
        }

        /**
         * @return the unmodifiable list of IDs of deselectable routes for the session.
         */
        @NonNull
        public List<String> getDeselectableRoutes() {
            synchronized (mLock) {
                return Collections.unmodifiableList(mSessionInfo.getDeselectableRoutes());
            }
        }

        /**
         * @return the unmodifiable list of IDs of groupable routes for the session.
         */
        @NonNull
        public List<String> getGroupableRoutes() {
            synchronized (mLock) {
                return Collections.unmodifiableList(mSessionInfo.getGroupableRoutes());
            }
        }

        /**
         * @return the unmodifiable list of IDs of transferrable routes for the session.
         */
        @NonNull
        public List<String> getTransferrableRoutes() {
            synchronized (mLock) {
                return Collections.unmodifiableList(mSessionInfo.getTransferrableRoutes());
            }
        }

        /**
         * Returns true if the session is released, false otherwise.
         * If it is released, then all other getters from this instance may return invalid values.
         * Also, any operations to this instance will be ignored once released.
         *
         * @see #release
         * @see SessionCallback#onSessionReleased
         */
        public boolean isReleased() {
            synchronized (mLock) {
                return mIsReleased;
            }
        }

        /**
         * Add routes to the remote session. Route add requests that are currently in
         * {@link #getSelectedRoutes()} will be ignored.
         *
         * @see #getSelectedRoutes()
         * @see SessionCallback#onSessionInfoChanged
         */
        public void addRoute(MediaRoute2Info route) {
            // TODO: Implement this when the actual connection logic is implemented.
        }

        /**
         * Remove routes from this session. Media may be stopped on those devices.
         * Route removal requests that are not currently in {@link #getSelectedRoutes()} will be
         * ignored.
         *
         * @see #getSelectedRoutes()
         * @see SessionCallback#onSessionInfoChanged
         */
        public void removeRoute(MediaRoute2Info route) {
            // TODO: Implement this when the actual connection logic is implemented.
        }

        /**
         * Release this session.
         * Any operation on this session after calling this method will be ignored.
         *
         * @param stopMedia Should the media that is playing on the device be stopped after this
         *                  session is released.
         * @see SessionCallback#onSessionReleased
         */
        public void release(boolean stopMedia) {
            synchronized (mLock) {
                if (mIsReleased) {
                    return;
                }
                mIsReleased = true;
            }
            // TODO: Use stopMedia variable when the actual connection logic is implemented.
        }
    }
    }


    final class RouteCallbackRecord {
    final class RouteCallbackRecord {
@@ -616,8 +778,7 @@ public class MediaRouter2 {
            if (!(obj instanceof RouteCallbackRecord)) {
            if (!(obj instanceof RouteCallbackRecord)) {
                return false;
                return false;
            }
            }
            return mRouteCallback
            return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback;
                    == ((RouteCallbackRecord) obj).mRouteCallback;
        }
        }


        @Override
        @Override
@@ -630,17 +791,17 @@ public class MediaRouter2 {
        public final MediaRoute2Info mRoute;
        public final MediaRoute2Info mRoute;
        public final String mControlCategory;
        public final String mControlCategory;
        public final Executor mExecutor;
        public final Executor mExecutor;
        public final SessionCreationCallback mSessionCreationCallback;
        public final SessionCallback mSessionCallback;
        public final int mRequestId;
        public final int mRequestId;


        SessionCreationRequest(int requestId, @NonNull MediaRoute2Info route,
        SessionCreationRequest(int requestId, @NonNull MediaRoute2Info route,
                @NonNull String controlCategory,
                @NonNull String controlCategory,
                @Nullable Executor executor,
                @Nullable Executor executor,
                @NonNull SessionCreationCallback sessionCreationCallback) {
                @NonNull SessionCallback sessionCallback) {
            mRoute = route;
            mRoute = route;
            mControlCategory = controlCategory;
            mControlCategory = controlCategory;
            mExecutor = executor;
            mExecutor = executor;
            mSessionCreationCallback = sessionCreationCallback;
            mSessionCallback = sessionCallback;
            mRequestId = requestId;
            mRequestId = requestId;
        }
        }
    }
    }
+0 −225
Original line number Original line Diff line number Diff line
/*
 * Copyright 2019 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.os.Bundle;


import com.android.internal.annotations.GuardedBy;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;

/**
 * A class to control media route session in media route provider.
 * For example, adding/removing/transferring routes to session can be done through this class.
 * Instances are created by {@link MediaRouter2}.
 *
 * TODO: When session is introduced, change Javadoc of all methods/classes by using [@link Session].
 *
 * @hide
 */
public class RouteSessionController {
    private final int mSessionId;
    private final String mCategory;
    private final Object mLock = new Object();
    private final Bundle mControlHints;

    private List<String> mSelectedRoutes;

    @GuardedBy("mLock")
    private final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords =
            new CopyOnWriteArrayList<>();

    private volatile boolean mIsReleased;

    /**
     * @param sessionInfo
     */
    RouteSessionController(@NonNull RouteSessionInfo sessionInfo) {
        mSessionId = sessionInfo.getSessionId();
        mCategory = sessionInfo.getControlCategory();
        mSelectedRoutes = sessionInfo.getSelectedRoutes();
        mControlHints = sessionInfo.getControlHints();
        // TODO: Create getters for all other types of routes
    }

    /**
     * @return the ID of this controller
     */
    public int getSessionId() {
        return mSessionId;
    }

    /**
     * @return the category of routes that the session includes.
     */
    @NonNull
    public String getCategory() {
        return mCategory;
    }

    /**
     * @return the control hints used to control route session if available.
     */
    @Nullable
    public Bundle getControlHints() {
        return mControlHints;
    }

    /**
     * @return the list of currently selected routes
     */
    @NonNull
    public List<String> getSelectedRoutes() {
        return Collections.unmodifiableList(mSelectedRoutes);
    }

    /**
     * Returns true if the session is released, false otherwise.
     * If it is released, then all other getters from this instance may return invalid values.
     * Also, any operations to this instance will be ignored once released.
     *
     * @see #release
     * @see Callback#onReleased
     */
    public boolean isReleased() {
        return mIsReleased;
    }

    /**
     * Add routes to the remote session.
     *
     * @see #getSelectedRoutes()
     * @see Callback#onSessionInfoChanged
     */
    public void addRoutes(List<MediaRoute2Info> routes) {
        // TODO: Implement this when the actual connection logic is implemented.
    }

    /**
     * Remove routes from this session. Media may be stopped on those devices.
     * Route removal requests that are not currently in {@link #getSelectedRoutes()} will be
     * ignored.
     *
     * @see #getSelectedRoutes()
     * @see Callback#onSessionInfoChanged
     */
    public void removeRoutes(List<MediaRoute2Info> routes) {
        // TODO: Implement this when the actual connection logic is implemented.
    }

    /**
     * Registers a {@link Callback} for monitoring route changes.
     * If the same callback is registered previously, previous executor will be overwritten with the
     * new one.
     */
    public void registerCallback(Executor executor, Callback callback) {
        if (mIsReleased) {
            return;
        }
        Objects.requireNonNull(executor, "executor must not be null");
        Objects.requireNonNull(callback, "callback must not be null");

        synchronized (mLock) {
            CallbackRecord recordWithSameCallback = null;
            for (CallbackRecord record : mCallbackRecords) {
                if (callback == record.mCallback) {
                    recordWithSameCallback = record;
                    break;
                }
            }

            if (recordWithSameCallback != null) {
                recordWithSameCallback.mExecutor = executor;
            } else {
                mCallbackRecords.add(new CallbackRecord(executor, callback));
            }
        }
    }

    /**
     * Unregisters a previously registered {@link Callback}.
     */
    public void unregisterCallback(Callback callback) {
        Objects.requireNonNull(callback, "callback must not be null");

        synchronized (mLock) {
            CallbackRecord recordToRemove = null;
            for (CallbackRecord record : mCallbackRecords) {
                if (callback == record.mCallback) {
                    recordToRemove = record;
                    break;
                }
            }

            if (recordToRemove != null) {
                mCallbackRecords.remove(recordToRemove);
            }
        }
    }

    /**
     * Release this session.
     * Any operation on this session after calling this method will be ignored.
     *
     * @param stopMedia Should the device where the media is played
     *                  be stopped after this session is released.
     */
    public void release(boolean stopMedia) {
        mIsReleased = true;
        mCallbackRecords.clear();
        // TODO: Use stopMedia variable when the actual connection logic is implemented.
    }

    /**
     * Callback class for getting updates on routes and session release.
     */
    public static class Callback {

        /**
         * Called when the session info has changed.
         * TODO: When SessionInfo is introduced, uncomment below argument.
         */
        void onSessionInfoChanged(/* SessionInfo info */) {}

        /**
         * Called when the session is released. Session can be released by the controller using
         * {@link #release(boolean)}, or by the {@link MediaRoute2ProviderService} itself.
         * One can do clean-ups here.
         *
         * TODO: When SessionInfo is introduced, change the javadoc of releasing session on
         * provider side.
         */
        void onReleased(int reason, boolean shouldStop) {}
    }

    private class CallbackRecord {
        public final Callback mCallback;
        public Executor mExecutor;

        CallbackRecord(@NonNull Executor executor, @NonNull Callback callback) {
            mExecutor = executor;
            mCallback = callback;
        }
    }
}
+6 −6
Original line number Original line Diff line number Diff line
@@ -43,8 +43,8 @@ import static org.testng.Assert.assertThrows;
import android.content.Context;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.MediaRouter2;
import android.media.MediaRouter2.SessionCreationCallback;
import android.media.MediaRouter2.RouteSessionController;
import android.media.RouteSessionController;
import android.media.MediaRouter2.SessionCallback;
import android.net.Uri;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcel;
import android.support.test.InstrumentationRegistry;
import android.support.test.InstrumentationRegistry;
@@ -209,7 +209,7 @@ public class MediaRouter2Test {
        MediaRoute2Info route = new MediaRoute2Info.Builder("id", "name").build();
        MediaRoute2Info route = new MediaRoute2Info.Builder("id", "name").build();
        String controlCategory = "controlCategory";
        String controlCategory = "controlCategory";
        Executor executor = mExecutor;
        Executor executor = mExecutor;
        MediaRouter2.SessionCreationCallback callback = new MediaRouter2.SessionCreationCallback();
        SessionCallback callback = new SessionCallback();


        // Tests null route
        // Tests null route
        assertThrows(NullPointerException.class,
        assertThrows(NullPointerException.class,
@@ -243,12 +243,12 @@ public class MediaRouter2Test {
        final CountDownLatch failureLatch = new CountDownLatch(1);
        final CountDownLatch failureLatch = new CountDownLatch(1);


        // Create session with this route
        // Create session with this route
        SessionCreationCallback callback = new SessionCreationCallback() {
        SessionCallback callback = new SessionCallback() {
            @Override
            @Override
            public void onSessionCreated(RouteSessionController controller) {
            public void onSessionCreated(RouteSessionController controller) {
                assertNotNull(controller);
                assertNotNull(controller);
                assertTrue(controller.getSelectedRoutes().contains(ROUTE_ID1));
                assertTrue(controller.getSelectedRoutes().contains(ROUTE_ID1));
                assertTrue(TextUtils.equals(CATEGORY_SAMPLE, controller.getCategory()));
                assertTrue(TextUtils.equals(CATEGORY_SAMPLE, controller.getControlCategory()));
                successLatch.countDown();
                successLatch.countDown();
            }
            }


@@ -281,7 +281,7 @@ public class MediaRouter2Test {
        final CountDownLatch failureLatch = new CountDownLatch(1);
        final CountDownLatch failureLatch = new CountDownLatch(1);


        // Create session with this route
        // Create session with this route
        SessionCreationCallback callback = new SessionCreationCallback() {
        SessionCallback callback = new SessionCallback() {
            @Override
            @Override
            public void onSessionCreated(RouteSessionController controller) {
            public void onSessionCreated(RouteSessionController controller) {
                successLatch.countDown();
                successLatch.countDown();