public abstract class

MediaRouteProvider

extends java.lang.Object

 java.lang.Object

↳androidx.mediarouter.media.MediaRouteProvider

Gradle dependencies

compile group: 'androidx.mediarouter', name: 'mediarouter', version: '1.3.0'

  • groupId: androidx.mediarouter
  • artifactId: mediarouter
  • version: 1.3.0

Artifact androidx.mediarouter:mediarouter:1.3.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.mediarouter:mediarouter com.android.support:mediarouter-v7

Androidx class mapping:

androidx.mediarouter.media.MediaRouteProvider android.support.v7.media.MediaRouteProvider

Overview

Media route providers are used to publish additional media routes for use within an application. Media route providers may also be declared as a service to publish additional media routes to all applications in the system.

The purpose of a media route provider is to discover media routes that satisfy the criteria specified by the current MediaRouteDiscoveryRequest and publish a MediaRouteProviderDescriptor with information about each route by calling MediaRouteProvider.setDescriptor(MediaRouteProviderDescriptor) to notify the currently registered MediaRouteProvider.Callback.

The provider should watch for changes to the discovery request by implementing MediaRouteProvider.onDiscoveryRequestChanged(MediaRouteDiscoveryRequest) and updating the set of routes that it is attempting to discover. It should also handle route control requests such as volume changes or media control intents by implementing MediaRouteProvider.onCreateRouteController(String) to return a MediaRouteProvider.RouteController for a particular route.

A media route provider can support dynamic group that allows the user to add or remove a route from the selected route dynamically. To control dynamic group, MediaRouteProvider.DynamicGroupRouteController returned by MediaRouteProvider.onCreateDynamicGroupRouteController(String) is used instead of MediaRouteProvider.RouteController.

A media route provider may be used privately within the scope of a single application process by calling MediaRouter.addProvider to add it to the local MediaRouter. A media route provider may also be made available globally to all applications by registering a MediaRouteProviderService in the provider's manifest. When the media route provider is registered as a service, all applications that use the media router API will be able to discover and used the provider's routes without having to install anything else.

This object must only be accessed on the main thread.

Summary

Constructors
publicMediaRouteProvider(Context context)

Creates a media route provider.

Methods
public final ContextgetContext()

Gets the context of the media route provider.

public final MediaRouteProviderDescriptorgetDescriptor()

Gets the provider's descriptor.

public final MediaRouteDiscoveryRequestgetDiscoveryRequest()

Gets the current discovery request which informs the provider about the kinds of routes to discover and whether to perform active scanning.

public final HandlergetHandler()

Gets the provider's handler which is associated with the main thread.

public final MediaRouteProvider.ProviderMetadatagetMetadata()

Gets some metadata about the provider's implementation.

public MediaRouteProvider.DynamicGroupRouteControlleronCreateDynamicGroupRouteController(java.lang.String initialMemberRouteId)

Creates a MediaRouteProvider.DynamicGroupRouteController.

public MediaRouteProvider.RouteControlleronCreateRouteController(java.lang.String routeId)

Called by the media router to obtain a route controller for a particular route.

public MediaRouteProvider.RouteControlleronCreateRouteController(java.lang.String routeId, java.lang.String routeGroupId)

Called by the media router to obtain a route controller for a particular route which is a member of .

public voidonDiscoveryRequestChanged(MediaRouteDiscoveryRequest request)

Called by the media router when the discovery request has changed.

public final voidsetCallback(MediaRouteProvider.Callback callback)

Sets a callback to invoke when the provider's descriptor changes.

public final voidsetDescriptor(MediaRouteProviderDescriptor descriptor)

Sets the provider's descriptor.

public final voidsetDiscoveryRequest(MediaRouteDiscoveryRequest request)

Sets a discovery request to inform the provider about the kinds of routes that its clients would like to discover and whether to perform active scanning.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MediaRouteProvider(Context context)

Creates a media route provider.

Parameters:

context: The context.

Methods

public final Context getContext()

Gets the context of the media route provider.

public final Handler getHandler()

Gets the provider's handler which is associated with the main thread.

public final MediaRouteProvider.ProviderMetadata getMetadata()

Gets some metadata about the provider's implementation.

public final void setCallback(MediaRouteProvider.Callback callback)

Sets a callback to invoke when the provider's descriptor changes.

Parameters:

callback: The callback to use, or null if none.

public final MediaRouteDiscoveryRequest getDiscoveryRequest()

Gets the current discovery request which informs the provider about the kinds of routes to discover and whether to perform active scanning.

Returns:

The current discovery request, or null if no discovery is needed at this time.

See also: MediaRouteProvider.onDiscoveryRequestChanged(MediaRouteDiscoveryRequest)

public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request)

Sets a discovery request to inform the provider about the kinds of routes that its clients would like to discover and whether to perform active scanning.

Parameters:

request: The discovery request, or null if no discovery is needed at this time.

See also: MediaRouteProvider.onDiscoveryRequestChanged(MediaRouteDiscoveryRequest)

public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request)

Called by the media router when the discovery request has changed.

Whenever an applications calls MediaRouter.addCallback(MediaRouteSelector, MediaRouter.Callback) to register a callback, it also provides a selector to specify the kinds of routes that it is interested in. The media router combines all of these selectors together to generate a MediaRouteDiscoveryRequest and notifies each provider when a change occurs by calling MediaRouteProvider.setDiscoveryRequest(MediaRouteDiscoveryRequest) which posts a message to invoke this method asynchronously.

The provider should examine the media control categories in the discovery request's selector to determine what kinds of routes it should try to discover and whether it should perform active or passive scans. In many cases, the provider may be able to save power by determining that the selector does not contain any categories that it supports and it can therefore avoid performing any scans at all.

Parameters:

request: The new discovery request, or null if no discovery is needed at this time.

See also: MediaRouter.addCallback(MediaRouteSelector, MediaRouter.Callback)

public final MediaRouteProviderDescriptor getDescriptor()

Gets the provider's descriptor.

The descriptor describes the state of the media route provider and the routes that it publishes. Watch for changes to the descriptor by registering a callback with MediaRouteProvider.setCallback(MediaRouteProvider.Callback).

Returns:

The media route provider descriptor, or null if none.

See also: MediaRouteProvider.Callback.onDescriptorChanged(MediaRouteProvider, MediaRouteProviderDescriptor)

public final void setDescriptor(MediaRouteProviderDescriptor descriptor)

Sets the provider's descriptor.

The provider must call this method to notify the currently registered callback about the change to the provider's descriptor.

Parameters:

descriptor: The updated route provider descriptor, or null if none.

See also: MediaRouteProvider.Callback.onDescriptorChanged(MediaRouteProvider, MediaRouteProviderDescriptor)

public MediaRouteProvider.RouteController onCreateRouteController(java.lang.String routeId)

Called by the media router to obtain a route controller for a particular route.

The media router will invoke the MediaRouteProvider.RouteController.onRelease() method of the route controller when it is no longer needed to allow it to free its resources.

Parameters:

routeId: The unique id of the route.

Returns:

The route controller. Returns null if there is no such route or if the route cannot be controlled using the route controller interface.

public MediaRouteProvider.RouteController onCreateRouteController(java.lang.String routeId, java.lang.String routeGroupId)

Called by the media router to obtain a route controller for a particular route which is a member of .

The media router will invoke the MediaRouteProvider.RouteController.onRelease() method of the route controller when it is no longer needed to allow it to free its resources.

Parameters:

routeId: The unique id of the member route.
routeGroupId: The unique id of the route group.

Returns:

The route controller. Returns null if there is no such route or if the route cannot be controlled using the route controller interface.

public MediaRouteProvider.DynamicGroupRouteController onCreateDynamicGroupRouteController(java.lang.String initialMemberRouteId)

Creates a MediaRouteProvider.DynamicGroupRouteController.

It will be called from an app or MediaRouter when a single route or a single static group is selected.

Parameters:

initialMemberRouteId: initially selected route's id.

Returns:

MediaRouteProvider.DynamicGroupRouteController. Returns null if there is no such route or if the route cannot be controlled using the MediaRouteProvider.DynamicGroupRouteController interface.

Source

/*
 * 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 static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.ObjectsCompat;
import androidx.mediarouter.media.MediaRouter.ControlRequestCallback;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * Media route providers are used to publish additional media routes for
 * use within an application.  Media route providers may also be declared
 * as a service to publish additional media routes to all applications
 * in the system.
 * <p>
 * The purpose of a media route provider is to discover media routes that satisfy
 * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
 * {@link MediaRouteProviderDescriptor} with information about each route by calling
 * {@link #setDescriptor} to notify the currently registered {@link Callback}.
 * </p><p>
 * The provider should watch for changes to the discovery request by implementing
 * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
 * attempting to discover.  It should also handle route control requests such
 * as volume changes or {@link MediaControlIntent media control intents}
 * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
 * for a particular route.
 * </p><p>
 * A media route provider can support
 * {@link MediaRouteProviderDescriptor#supportsDynamicGroupRoute dynamic group} that
 * allows the user to add or remove a route from the selected route dynamically.
 * To control dynamic group, {@link DynamicGroupRouteController} returned by
 * {@link #onCreateDynamicGroupRouteController} is used instead of {@link RouteController}.
 * </p><p>
 * A media route provider may be used privately within the scope of a single
 * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
 * to add it to the local {@link MediaRouter}.  A media route provider may also be made
 * available globally to all applications by registering a {@link MediaRouteProviderService}
 * in the provider's manifest.  When the media route provider is registered
 * as a service, all applications that use the media router API will be able to
 * discover and used the provider's routes without having to install anything else.
 * </p><p>
 * This object must only be accessed on the main thread.
 * </p>
 */
public abstract class MediaRouteProvider {
    static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
    static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;

    private final Context mContext;
    private final ProviderMetadata mMetadata;
    private final ProviderHandler mHandler = new ProviderHandler();

    private Callback mCallback;

    private MediaRouteDiscoveryRequest mDiscoveryRequest;
    private boolean mPendingDiscoveryRequestChange;

    private MediaRouteProviderDescriptor mDescriptor;
    private boolean mPendingDescriptorChange;

    /**
     * Creates a media route provider.
     *
     * @param context The context.
     */
    public MediaRouteProvider(@NonNull Context context) {
        this(context, null);
    }

    MediaRouteProvider(Context context, ProviderMetadata metadata) {
        if (context == null) {
            throw new IllegalArgumentException("context must not be null");
        }

        mContext = context;
        if (metadata == null) {
            mMetadata = new ProviderMetadata(new ComponentName(context, getClass()));
        } else {
            mMetadata = metadata;
        }
    }

    /**
     * Gets the context of the media route provider.
     */
    @NonNull
    public final Context getContext() {
        return mContext;
    }

    /**
     * Gets the provider's handler which is associated with the main thread.
     */
    @NonNull
    public final Handler getHandler() {
        return mHandler;
    }

    /**
     * Gets some metadata about the provider's implementation.
     */
    @NonNull
    public final ProviderMetadata getMetadata() {
        return mMetadata;
    }

    /**
     * Sets a callback to invoke when the provider's descriptor changes.
     *
     * @param callback The callback to use, or null if none.
     */
    public final void setCallback(@Nullable Callback callback) {
        MediaRouter.checkCallingThread();
        mCallback = callback;
    }

    /**
     * Gets the current discovery request which informs the provider about the
     * kinds of routes to discover and whether to perform active scanning.
     *
     * @return The current discovery request, or null if no discovery is needed at this time.
     *
     * @see #onDiscoveryRequestChanged
     */
    @Nullable
    public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
        return mDiscoveryRequest;
    }

    /**
     * Sets a discovery request to inform the provider about the kinds of
     * routes that its clients would like to discover and whether to perform active scanning.
     *
     * @param request The discovery request, or null if no discovery is needed at this time.
     *
     * @see #onDiscoveryRequestChanged
     */
    public final void setDiscoveryRequest(@Nullable MediaRouteDiscoveryRequest request) {
        MediaRouter.checkCallingThread();

        if (ObjectsCompat.equals(mDiscoveryRequest, request)) {
            return;
        }

        setDiscoveryRequestInternal(request);
    }

    final void setDiscoveryRequestInternal(@Nullable MediaRouteDiscoveryRequest request) {
        mDiscoveryRequest = request;
        if (!mPendingDiscoveryRequestChange) {
            mPendingDiscoveryRequestChange = true;
            mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
        }
    }

    void deliverDiscoveryRequestChanged() {
        mPendingDiscoveryRequestChange = false;
        onDiscoveryRequestChanged(mDiscoveryRequest);
    }

    /**
     * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
     * has changed.
     * <p>
     * Whenever an applications calls {@link MediaRouter#addCallback} to register
     * a callback, it also provides a selector to specify the kinds of routes that
     * it is interested in.  The media router combines all of these selectors together
     * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
     * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
     * this method asynchronously.
     * </p><p>
     * The provider should examine the {@link MediaControlIntent media control categories}
     * in the discovery request's {@link MediaRouteSelector selector} to determine what
     * kinds of routes it should try to discover and whether it should perform active
     * or passive scans.  In many cases, the provider may be able to save power by
     * determining that the selector does not contain any categories that it supports
     * and it can therefore avoid performing any scans at all.
     * </p>
     *
     * @param request The new discovery request, or null if no discovery is needed at this time.
     *
     * @see MediaRouter#addCallback
     */
    public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
    }

    /**
     * Gets the provider's descriptor.
     * <p>
     * The descriptor describes the state of the media route provider and
     * the routes that it publishes.  Watch for changes to the descriptor
     * by registering a {@link Callback callback} with {@link #setCallback}.
     * </p>
     *
     * @return The media route provider descriptor, or null if none.
     *
     * @see Callback#onDescriptorChanged
     */
    @Nullable
    public final MediaRouteProviderDescriptor getDescriptor() {
        return mDescriptor;
    }

    /**
     * Sets the provider's descriptor.
     * <p>
     * The provider must call this method to notify the currently registered
     * {@link Callback callback} about the change to the provider's descriptor.
     * </p>
     *
     * @param descriptor The updated route provider descriptor, or null if none.
     *
     * @see Callback#onDescriptorChanged
     */
    public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
        MediaRouter.checkCallingThread();

        if (mDescriptor != descriptor) {
            mDescriptor = descriptor;
            if (!mPendingDescriptorChange) {
                mPendingDescriptorChange = true;
                mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
            }
        }
    }

    void deliverDescriptorChanged() {
        mPendingDescriptorChange = false;

        if (mCallback != null) {
            mCallback.onDescriptorChanged(this, mDescriptor);
        }
    }

    /**
     * Called by the media router to obtain a route controller for a particular route.
     * <p>
     * The media router will invoke the {@link RouteController#onRelease} method of the route
     * controller when it is no longer needed to allow it to free its resources.
     * </p>
     *
     * @param routeId The unique id of the route.
     * @return The route controller.  Returns null if there is no such route or if the route
     * cannot be controlled using the route controller interface.
     */
    @Nullable
    public RouteController onCreateRouteController(@NonNull String routeId) {
        if (routeId == null) {
            throw new IllegalArgumentException("routeId cannot be null");
        }
        return null;
    }

    /**
     * Called by the media router to obtain a route controller for a particular route which is a
     * member of {@link MediaRouter.RouteGroup}.
     * <p>
     * The media router will invoke the {@link RouteController#onRelease} method of the route
     * controller when it is no longer needed to allow it to free its resources.
     * </p>
     *
     * @param routeId The unique id of the member route.
     * @param routeGroupId The unique id of the route group.
     * @return The route controller.  Returns null if there is no such route or if the route
     * cannot be controlled using the route controller interface.
     * @hide
     */
    @RestrictTo(LIBRARY)
    @Nullable
    public RouteController onCreateRouteController(@NonNull String routeId,
            @NonNull String routeGroupId) {
        if (routeId == null) {
            throw new IllegalArgumentException("routeId cannot be null");
        }
        if (routeGroupId == null) {
            throw new IllegalArgumentException("routeGroupId cannot be null");
        }
        return onCreateRouteController(routeId);
    }

    /**
     * Creates a {@link DynamicGroupRouteController}.
     * <p>
     * It will be called from an app or {@link MediaRouter} when a single route or a single static
     * group is selected.
     * </p>
     *
     * @param initialMemberRouteId initially selected route's id.
     * @return {@link DynamicGroupRouteController}. Returns null if there is no such route or
     * if the route cannot be controlled using the {@link DynamicGroupRouteController} interface.
     */
    @Nullable
    public DynamicGroupRouteController onCreateDynamicGroupRouteController(
            @NonNull String initialMemberRouteId) {
        if (initialMemberRouteId == null) {
            throw new IllegalArgumentException("initialMemberRouteId cannot be null.");
        }
        return null;
    }

    /**
     * Describes properties of the route provider's implementation.
     * <p>
     * This object is immutable once created.
     * </p>
     */
    public static final class ProviderMetadata {
        private final ComponentName mComponentName;

        ProviderMetadata(ComponentName componentName) {
            if (componentName == null) {
                throw new IllegalArgumentException("componentName must not be null");
            }
            mComponentName = componentName;
        }

        /**
         * Gets the provider's package name.
         */
        @NonNull
        public String getPackageName() {
            return mComponentName.getPackageName();
        }

        /**
         * Gets the provider's component name.
         */
        @NonNull
        public ComponentName getComponentName() {
            return mComponentName;
        }

        @Override
        public String toString() {
            return "ProviderMetadata{ componentName="
                    + mComponentName.flattenToShortString() + " }";
        }
    }

    /**
     * Provides control over a particular route.
     * <p>
     * The media router obtains a route controller for a route whenever it needs
     * to control a route.  When a route is selected, the media router invokes
     * the {@link #onSelect} method of its route controller.  While selected,
     * the media router may call other methods of the route controller to
     * request that it perform certain actions to the route.  When a route is
     * unselected, the media router invokes the {@link #onUnselect(int)} method of its
     * route controller.  When the media route no longer needs the route controller
     * it will invoke the {@link #onRelease} method to allow the route controller
     * to free its resources.
     * </p><p>
     * There may be multiple route controllers simultaneously active for the
     * same route.  Each route controller will be released separately.
     * </p><p>
     * All operations on the route controller are asynchronous and
     * results are communicated via callbacks.
     * </p>
     */
    public static abstract class RouteController {
        /**
         * Releases the route controller, allowing it to free its resources.
         */
        public void onRelease() {
        }

        /**
         * Selects the route.
         */
        public void onSelect() {
        }

        /**
         * Unselects the route.
         *
         * @deprecated Use {@link #onUnselect(int)} instead.
         */
        @Deprecated
        public void onUnselect() {
        }

        /**
         * Unselects the route and provides a reason. The default implementation
         * calls {@link #onUnselect()}.
         * <p>
         * The reason provided will be one of the following:
         * <ul>
         * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
         * </ul>
         *
         * @param reason The reason for unselecting the route.
         */
        public void onUnselect(@MediaRouter.UnselectReason int reason) {
            onUnselect();
        }

        /**
         * Requests to set the volume of the route.
         *
         * @param volume The new volume value between 0 and
         * {@link MediaRouteDescriptor#getVolumeMax}.
         */
        public void onSetVolume(int volume) {
        }

        /**
         * Requests an incremental volume update for the route.
         *
         * @param delta The delta to add to the current volume.
         */
        public void onUpdateVolume(int delta) {
        }

        /**
         * Performs a {@link MediaControlIntent media control} request
         * asynchronously on behalf of the route.
         *
         * @param intent A {@link MediaControlIntent media control intent}.
         * @param callback A {@link ControlRequestCallback} to invoke with the result
         * of the request, or null if no result is required.
         * @return True if the controller intends to handle the request and will
         * invoke the callback when finished.  False if the controller will not
         * handle the request and will not invoke the callback.
         *
         * @see MediaControlIntent
         */
        public boolean onControlRequest(@NonNull Intent intent,
                @Nullable ControlRequestCallback callback) {
            return false;
        }
    }

    /**
     * Provides control over a dynamic group route.
     * A dynamic group route is a group of routes such that a route can be added or removed
     * from the group by the user dynamically.
     */
    public abstract static class DynamicGroupRouteController extends RouteController {
        private final Object mLock = new Object();

        @GuardedBy("mLock")
        Executor mExecutor;
        @GuardedBy("mLock")
        OnDynamicRoutesChangedListener mListener;
        @GuardedBy("mLock")
        MediaRouteDescriptor mPendingGroupRoute;
        @GuardedBy("mLock")
        Collection<DynamicRouteDescriptor> mPendingRoutes;

        /**
         * Gets the title of the groupable routes section which will be shown to the user.
         * It is provided by {@link MediaRouteProvider}.
         * e.g. "Add a device."
         */
        @Nullable
        public String getGroupableSelectionTitle() {
            return null;
        }

        /**
         * Gets the title of the transferable routes section which will be shown to the user.
         * It is provided by {@link MediaRouteProvider}.
         * {@link MediaRouteProvider}.
         * e.g. "Play on group."
         */
        @Nullable
        public String getTransferableSectionTitle() {
            return null;
        }

        /**
         * Called when a user selects a new set of routes s/he wants the session to be played.
         */
        public abstract void onUpdateMemberRoutes(@Nullable List<String> routeIds);

        /**
         * Called when a user adds a route into the casting session.
         */
        public abstract void onAddMemberRoute(@NonNull String routeId);

        /**
         * Called when a user removes a route from casting session.
         */
        public abstract void onRemoveMemberRoute(@NonNull String routeId);

        /**
         * Called by {@link MediaRouter} to set the listener.
         */
        void setOnDynamicRoutesChangedListener(
                @NonNull Executor executor,
                @NonNull OnDynamicRoutesChangedListener listener) {
            synchronized (mLock) {
                if (executor == null) {
                    throw new NullPointerException("Executor shouldn't be null");
                }
                if (listener == null) {
                    throw new NullPointerException("Listener shouldn't be null");
                }
                mExecutor = executor;
                mListener = listener;

                if (mPendingRoutes != null && !mPendingRoutes.isEmpty()) {
                    MediaRouteDescriptor groupRoute = mPendingGroupRoute;
                    Collection<DynamicRouteDescriptor> routes = mPendingRoutes;
                    mPendingGroupRoute = null;
                    mPendingRoutes = null;
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            listener.onRoutesChanged(DynamicGroupRouteController.this,
                                    groupRoute,
                                    routes);
                        }
                    });
                }
            }
        }

        /**
         * Sets the dynamic route descriptors for the dynamic group.
         * <p>
         * The dynamic group controller should call this method to notify the current
         * dynamic group state.
         * </p>
         * @param routes The dynamic route descriptors for published routes.
         *               At least a selected or selecting route must be included.
         * @deprecated Use {@link #notifyDynamicRoutesChanged(MediaRouteDescriptor, Collection)}
         * instead to notify information of the group.
         */
        @Deprecated
        public final void notifyDynamicRoutesChanged(
                @NonNull final Collection<DynamicRouteDescriptor> routes) {
            if (routes == null) {
                throw new NullPointerException("routes must not be null");
            }
            synchronized (mLock) {
                if (mExecutor != null) {
                    final OnDynamicRoutesChangedListener listener = mListener;
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            listener.onRoutesChanged(
                                    DynamicGroupRouteController.this,
                                    null,
                                    routes);
                        }
                    });
                } else {
                    mPendingRoutes = new ArrayList<>(routes);
                }
            }
        }

        /**
         * Sets the group route descriptor and the dynamic route descriptors for the dynamic group.
         * <p>
         * The dynamic group controller should call this method to notify the current
         * dynamic group state.
         * </p>
         * @param groupRoute The media route descriptor describing the dynamic group.
         *                   The {@link MediaRouter#getSelectedRoute() selected route} of the
         *                   media router will contain this information.
         *                   If it is {@link MediaRouteDescriptor#isEnabled() disabled},
         *                   the media router will unselect the dynamic group and release
         *                   the route controller.
         * @param dynamicRoutes The dynamic route descriptors for published routes.
         *                      At least a selected or selecting route should be included.
         */
        public final void notifyDynamicRoutesChanged(
                @NonNull MediaRouteDescriptor groupRoute,
                @NonNull Collection<DynamicRouteDescriptor> dynamicRoutes) {
            if (groupRoute == null) {
                throw new NullPointerException("groupRoute must not be null");
            }
            if (dynamicRoutes == null) {
                throw new NullPointerException("dynamicRoutes must not be null");
            }
            synchronized (mLock) {
                if (mExecutor != null) {
                    final OnDynamicRoutesChangedListener listener = mListener;
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            listener.onRoutesChanged(
                                    DynamicGroupRouteController.this,
                                    groupRoute,
                                    dynamicRoutes);
                        }
                    });
                } else {
                    mPendingGroupRoute = groupRoute;
                    mPendingRoutes = new ArrayList<>(dynamicRoutes);
                }
            }
        }

        /**
         * Used to notify media router each route's property changes regarding this
         * {@link DynamicGroupRouteController} instance.
         * <p> Here are some examples when this notification is called :
         * <ul>
         *     <li> a route is newly turned on and it can be grouped with this dynamic group route.
         *     </li>
         *     <li> a route is selecting as a member of this dynamic group route.</li>
         *     <li> a route is selected as a member of this dynamic group route.</li>
         *     <li> a route is unselecting.</li>
         *     <li> a route is unselected.</li>
         *     <li> a route is turned off.</li>
         * </ul>
         * </p>
         */
        interface OnDynamicRoutesChangedListener {
            /**
             * The provider should call this method when routes' properties change.
             * (e.g. when a route becomes ungroupable)
             *
             * @param controller the {@link DynamicGroupRouteController} which keeps this listener.
             * @param groupRoute the route descriptor about the dynamic group.
             * @param routes the collection of routes contains selected routes.
             *               (can be unselectable or not)
             *               and unselected routes (can be groupable or transferable or not).
             */
            void onRoutesChanged(
                    DynamicGroupRouteController controller,
                    MediaRouteDescriptor groupRoute,
                    Collection<DynamicRouteDescriptor> routes);
        }

        /**
         * Contains a route, its selection state and its capabilities.
         */
        public static final class DynamicRouteDescriptor {
            static final String KEY_MEDIA_ROUTE_DESCRIPTOR = "mrDescriptor";
            static final String KEY_SELECTION_STATE = "selectionState";
            static final String KEY_IS_UNSELECTABLE = "isUnselectable";
            static final String KEY_IS_GROUPABLE = "isGroupable";
            static final String KEY_IS_TRANSFERABLE = "isTransferable";

            /**
             * @hide
             */
            @RestrictTo(LIBRARY)
            @IntDef({
                    UNSELECTING,
                    UNSELECTED,
                    SELECTING,
                    SELECTED
            })
            @Retention(RetentionPolicy.SOURCE)
            public @interface SelectionState {}
            /**
             * After a user unselects a route, it might take some time for a provider to complete
             * the operation. This state is used in this between time. MediaRouter can either
             * block the UI or show the route as unchecked.
             */
            public static final int UNSELECTING = 0;

            /**
             * The route is unselected.
             * <p>
             * Unselect operation is done by the route provider.
             * </p>
             */
            public static final int UNSELECTED = 1;

            /**
             * After a user selects a route, it might take some time for a provider to complete
             * the operation. This state is used in this between time. MediaRouter can either
             * block the UI or show the route as checked.
             */
            public static final int SELECTING = 2;

            /**
             * The route is selected.
             * <p>
             * Select operation is done by the route provider.
             * </p>
             */
            public static final int SELECTED = 3;

            //TODO: mMediaRouteDescriptor could have an old info. We should provide a way to
            // update it or use only the route ID.
            final MediaRouteDescriptor mMediaRouteDescriptor;
            @SelectionState
            final int mSelectionState;
            final boolean mIsUnselectable;
            final boolean mIsGroupable;
            final boolean mIsTransferable;
            Bundle mBundle;

            DynamicRouteDescriptor(
                    MediaRouteDescriptor mediaRouteDescriptor, @SelectionState int selectionState,
                    boolean isUnselectable, boolean isGroupable, boolean isTransferable) {
                mMediaRouteDescriptor = mediaRouteDescriptor;
                mSelectionState = selectionState;
                mIsUnselectable = isUnselectable;
                mIsGroupable = isGroupable;
                mIsTransferable = isTransferable;
            }

            /**
             * Gets this route's {@link MediaRouteDescriptor}. i.e. which route this info is for.
             */
            @NonNull
            public MediaRouteDescriptor getRouteDescriptor() {
                return mMediaRouteDescriptor;
            }

            /**
             * Gets the selection state.
             */
            public @SelectionState int getSelectionState() {
                return mSelectionState;
            }

            /**
             * Returns true if the route can be unselected.
             * <p>
             * For example, a static group has an old build which doesn't support dynamic group.
             * All its members can't be removed.
             * </p>
             * <p>
             * Only applicable to selected/selecting routes.
             * </p>
             */
            public boolean isUnselectable() {
                return mIsUnselectable;
            }

            /**
             * Returns true if the route can be grouped into the dynamic group route.
             * <p>
             * Only applicable to unselected/unselecting routes.
             * Note that {@link #isGroupable()} and {@link #isTransferable()} are NOT mutually
             * exclusive.
             * </p>
             */
            public boolean isGroupable() {
                return mIsGroupable;
            }

            /**
             * Returns true if the current dynamic group route can be transferred to this route.
             * <p>
             * Only applicable to unselected/unselecting routes.
             * Note that {@link #isGroupable()} and {@link #isTransferable()} are NOT mutually
             * exclusive.
             * </p>
             */
            public boolean isTransferable() {
                return mIsTransferable;
            }

            Bundle toBundle() {
                if (mBundle == null) {
                    mBundle = new Bundle();
                    mBundle.putBundle(KEY_MEDIA_ROUTE_DESCRIPTOR, mMediaRouteDescriptor.asBundle());
                    mBundle.putInt(KEY_SELECTION_STATE, mSelectionState);
                    mBundle.putBoolean(KEY_IS_UNSELECTABLE, mIsUnselectable);
                    mBundle.putBoolean(KEY_IS_GROUPABLE, mIsGroupable);
                    mBundle.putBoolean(KEY_IS_TRANSFERABLE, mIsTransferable);
                }
                return mBundle;
            }

            static DynamicRouteDescriptor fromBundle(Bundle bundle) {
                if (bundle == null) {
                    return null;
                }
                MediaRouteDescriptor descriptor = MediaRouteDescriptor.fromBundle(
                        bundle.getBundle(KEY_MEDIA_ROUTE_DESCRIPTOR));
                int selectionState = bundle.getInt(KEY_SELECTION_STATE, UNSELECTED);
                boolean isUnselectable = bundle.getBoolean(KEY_IS_UNSELECTABLE, false);
                boolean isGroupable = bundle.getBoolean(KEY_IS_GROUPABLE, false);
                boolean isTransferable = bundle.getBoolean(KEY_IS_TRANSFERABLE, false);
                return new DynamicRouteDescriptor(descriptor, selectionState, isUnselectable,
                        isGroupable, isTransferable);
            }

            /**
             * Builder for {@link DynamicRouteDescriptor}
             */
            public static final class  Builder {
                private final MediaRouteDescriptor mRouteDescriptor;
                private @SelectionState int mSelectionState = UNSELECTED;
                private boolean mIsUnselectable = false;
                private boolean mIsGroupable = false;
                private boolean mIsTransferable = false;

                /**
                 * A constructor with {@link MediaRouteDescriptor}.
                 */
                public Builder(@NonNull MediaRouteDescriptor descriptor) {
                    if (descriptor == null) {
                        throw new NullPointerException("descriptor must not be null");
                    }
                    mRouteDescriptor = descriptor;
                }

                /**
                 * Copies the properties from the given {@link DynamicRouteDescriptor}
                 */
                public Builder(@NonNull DynamicRouteDescriptor dynamicRouteDescriptor) {
                    if (dynamicRouteDescriptor == null) {
                        throw new NullPointerException("dynamicRouteDescriptor must not be null");
                    }
                    mRouteDescriptor = dynamicRouteDescriptor.getRouteDescriptor();
                    mSelectionState = dynamicRouteDescriptor.getSelectionState();
                    mIsUnselectable = dynamicRouteDescriptor.isUnselectable();
                    mIsGroupable = dynamicRouteDescriptor.isGroupable();
                    mIsTransferable = dynamicRouteDescriptor.isTransferable();
                }

                /**
                 * Sets the selection state of this route within the associated dynamic group route.
                 */
                @NonNull
                public Builder setSelectionState(@SelectionState int state) {
                    mSelectionState = state;
                    return this;
                }

                /**
                 * Sets if this route can be unselected.
                 */
                @NonNull
                public Builder setIsUnselectable(boolean value) {
                    mIsUnselectable = value;
                    return this;
                }

                /**
                 * Sets if this route can be a selected as a member of the associated dynamic
                 * group route.
                 */
                @NonNull
                public Builder setIsGroupable(boolean value) {
                    mIsGroupable = value;
                    return this;
                }

                /**
                 * Sets if the associated dynamic group route can be transferred to this route.
                 */
                @NonNull
                public Builder setIsTransferable(boolean value) {
                    mIsTransferable = value;
                    return this;
                }

                /**
                 * Builds the {@link DynamicRouteDescriptor}.
                 */
                @NonNull
                public DynamicRouteDescriptor build() {
                    return new DynamicRouteDescriptor(
                            mRouteDescriptor, mSelectionState, mIsUnselectable, mIsGroupable,
                            mIsTransferable);
                }
            }
        }
    }

    /**
     * Callback which is invoked when route information becomes available or changes.
     */
    public static abstract class Callback {
        /**
         * Called when information about a route provider and its routes change.
         *
         * @param provider The media route provider that changed, never null.
         * @param descriptor The new media route provider descriptor, or null if none.
         */
        public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
                @Nullable MediaRouteProviderDescriptor descriptor) {
        }
    }

    private final class ProviderHandler extends Handler {
        ProviderHandler() {
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DELIVER_DESCRIPTOR_CHANGED:
                    deliverDescriptorChanged();
                    break;
                case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
                    deliverDiscoveryRequestChanged();
                    break;
            }
        }
    }
}