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

Commit dd4f9e8b authored by Jeff Brown's avatar Jeff Brown Committed by Android (Google) Code Review
Browse files

Merge "Update the media router dialogs and integrate into system UI." into klp-dev

parents 12c32b4d 0abd3a6c
Loading
Loading
Loading
Loading
+92 −78
Original line number Diff line number Diff line
@@ -16,10 +16,7 @@

package android.app;

import com.android.internal.app.MediaRouteChooserDialogFragment;

import android.content.Context;
import android.content.ContextWrapper;
import android.media.MediaRouter;
import android.media.MediaRouter.RouteInfo;
import android.util.Log;
@@ -30,22 +27,38 @@ import android.view.ViewGroup;

import java.lang.ref.WeakReference;

/**
 * The media route action provider displays a {@link MediaRouteButton media route button}
 * in the application's {@link ActionBar} to allow the user to select routes and
 * to control the currently selected route.
 * <p>
 * The application must specify the kinds of routes that the user should be allowed
 * to select by specifying the route types with the {@link #setRouteTypes} method.
 * </p><p>
 * Refer to {@link MediaRouteButton} for a description of the button that will
 * appear in the action bar menu.  Note that instead of disabling the button
 * when no routes are available, the action provider will instead make the
 * menu item invisible.  In this way, the button will only be visible when it
 * is possible for the user to discover and select a matching route.
 * </p>
 */
public class MediaRouteActionProvider extends ActionProvider {
    private static final String TAG = "MediaRouteActionProvider";

    private Context mContext;
    private MediaRouter mRouter;
    private MenuItem mMenuItem;
    private MediaRouteButton mView;
    private final Context mContext;
    private final MediaRouter mRouter;
    private final MediaRouterCallback mCallback;

    private int mRouteTypes;
    private MediaRouteButton mButton;
    private View.OnClickListener mExtendedSettingsListener;
    private RouterCallback mCallback;

    public MediaRouteActionProvider(Context context) {
        super(context);

        mContext = context;
        mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
        mCallback = new RouterCallback(this);
        mCallback = new MediaRouterCallback(this);

        // Start with live audio by default.
        // TODO Update this when new route types are added; segment by API level
@@ -53,80 +66,74 @@ public class MediaRouteActionProvider extends ActionProvider {
        setRouteTypes(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
    }

    /**
     * Sets the types of routes that will be shown in the media route chooser dialog
     * launched by this button.
     *
     * @param types The route types to match.
     */
    public void setRouteTypes(int types) {
        if (mRouteTypes == types) return;
        if (mRouteTypes != types) {
            // FIXME: We currently have no way of knowing whether the action provider
            // is still needed by the UI.  Unfortunately this means the action provider
            // may leak callbacks until garbage collection occurs.  This may result in
            // media route providers doing more work than necessary in the short term
            // while trying to discover routes that are no longer of interest to the
            // application.  To solve this problem, the action provider will need some
            // indication from the framework that it is being destroyed.
            if (mRouteTypes != 0) {
                mRouter.removeCallback(mCallback);
            }
            mRouteTypes = types;
            if (types != 0) {
            mRouter.addCallback(types, mCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
                mRouter.addCallback(types, mCallback,
                        MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
            }
            refreshRoute();

            if (mButton != null) {
                mButton.setRouteTypes(mRouteTypes);
            }
        if (mView != null) {
            mView.setRouteTypes(mRouteTypes);
        }
    }

    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
        mExtendedSettingsListener = listener;
        if (mButton != null) {
            mButton.setExtendedSettingsClickListener(listener);
        }
    }

    @Override
    @SuppressWarnings("deprecation")
    public View onCreateActionView() {
        throw new UnsupportedOperationException("Use onCreateActionView(MenuItem) instead.");
    }

    @Override
    public View onCreateActionView(MenuItem item) {
        if (mMenuItem != null || mView != null) {
        if (mButton != null) {
            Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
                    "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
                    "Abandoning the old one...");
        }
        mMenuItem = item;
        mView = new MediaRouteButton(mContext);
        mView.setCheatSheetEnabled(true);
        mView.setRouteTypes(mRouteTypes);
        mView.setExtendedSettingsClickListener(mExtendedSettingsListener);
        mView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,

        mButton = new MediaRouteButton(mContext);
        mButton.setCheatSheetEnabled(true);
        mButton.setRouteTypes(mRouteTypes);
        mButton.setExtendedSettingsClickListener(mExtendedSettingsListener);
        mButton.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        return mView;
        return mButton;
    }

    @Override
    public boolean onPerformDefaultAction() {
        final FragmentManager fm = getActivity().getFragmentManager();
        // See if one is already attached to this activity.
        MediaRouteChooserDialogFragment dialogFragment =
                (MediaRouteChooserDialogFragment) fm.findFragmentByTag(
                MediaRouteChooserDialogFragment.FRAGMENT_TAG);
        if (dialogFragment != null) {
            Log.w(TAG, "onPerformDefaultAction(): Chooser dialog already showing!");
            return false;
        }

        dialogFragment = new MediaRouteChooserDialogFragment();
        dialogFragment.setExtendedSettingsClickListener(mExtendedSettingsListener);
        dialogFragment.setRouteTypes(mRouteTypes);
        dialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG);
        return true;
    }

    private Activity getActivity() {
        // Gross way of unwrapping the Activity so we can get the FragmentManager
        Context context = mContext;
        while (context instanceof ContextWrapper && !(context instanceof Activity)) {
            context = ((ContextWrapper) context).getBaseContext();
        }
        if (!(context instanceof Activity)) {
            throw new IllegalStateException("The MediaRouteActionProvider's Context " +
                    "is not an Activity.");
        }

        return (Activity) context;
    }

    public void setExtendedSettingsClickListener(View.OnClickListener listener) {
        mExtendedSettingsListener = listener;
        if (mView != null) {
            mView.setExtendedSettingsClickListener(listener);
        if (mButton != null) {
            return mButton.showDialogInternal();
        }
        return false;
    }

    @Override
@@ -136,36 +143,43 @@ public class MediaRouteActionProvider extends ActionProvider {

    @Override
    public boolean isVisible() {
        return mRouter.getRouteCount() > 1;
        return mRouter.isRouteAvailable(mRouteTypes,
                MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
    }

    private static class RouterCallback extends MediaRouter.SimpleCallback {
        private WeakReference<MediaRouteActionProvider> mAp;
    private void refreshRoute() {
        refreshVisibility();
    }

    private static class MediaRouterCallback extends MediaRouter.SimpleCallback {
        private final WeakReference<MediaRouteActionProvider> mProviderWeak;

        RouterCallback(MediaRouteActionProvider ap) {
            mAp = new WeakReference<MediaRouteActionProvider>(ap);
        public MediaRouterCallback(MediaRouteActionProvider provider) {
            mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
        }

        @Override
        public void onRouteAdded(MediaRouter router, RouteInfo info) {
            final MediaRouteActionProvider ap = mAp.get();
            if (ap == null) {
                router.removeCallback(this);
                return;
            refreshRoute(router);
        }

            ap.refreshVisibility();
        @Override
        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
            refreshRoute(router);
        }

        @Override
        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
            final MediaRouteActionProvider ap = mAp.get();
            if (ap == null) {
                router.removeCallback(this);
                return;
        public void onRouteChanged(MediaRouter router, RouteInfo info) {
            refreshRoute(router);
        }

            ap.refreshVisibility();
        private void refreshRoute(MediaRouter router) {
            MediaRouteActionProvider provider = mProviderWeak.get();
            if (provider != null) {
                provider.refreshRoute();
            } else {
                router.removeCallback(this);
            }
        }
    }
}
+144 −178
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
package android.app;

import com.android.internal.R;
import com.android.internal.app.MediaRouteChooserDialogFragment;
import com.android.internal.app.MediaRouteDialogPresenter;

import android.content.Context;
import android.content.ContextWrapper;
@@ -30,7 +30,6 @@ import android.media.MediaRouter.RouteGroup;
import android.media.MediaRouter.RouteInfo;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.SoundEffectConstants;
@@ -38,17 +37,15 @@ import android.view.View;
import android.widget.Toast;

public class MediaRouteButton extends View {
    private static final String TAG = "MediaRouteButton";
    private final MediaRouter mRouter;
    private final MediaRouterCallback mCallback;

    private MediaRouter mRouter;
    private final MediaRouteCallback mRouterCallback = new MediaRouteCallback();
    private int mRouteTypes;

    private boolean mAttachedToWindow;

    private Drawable mRemoteIndicator;
    private boolean mRemoteActive;
    private boolean mToggleMode;
    private boolean mCheatSheetEnabled;
    private boolean mIsConnecting;

@@ -56,12 +53,13 @@ public class MediaRouteButton extends View {
    private int mMinHeight;

    private OnClickListener mExtendedSettingsClickListener;
    private MediaRouteChooserDialogFragment mDialogFragment;

    // The checked state is used when connected to a remote route.
    private static final int[] CHECKED_STATE_SET = {
        R.attr.state_checked
    };

    // The activated state is used while connecting to a remote route.
    private static final int[] ACTIVATED_STATE_SET = {
        R.attr.state_activated
    };
@@ -78,6 +76,7 @@ public class MediaRouteButton extends View {
        super(context, attrs, defStyleAttr);

        mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
        mCallback = new MediaRouterCallback();

        TypedArray a = context.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0);
@@ -98,53 +97,99 @@ public class MediaRouteButton extends View {
        setRouteTypes(routeTypes);
    }

    private void setRemoteIndicatorDrawable(Drawable d) {
        if (mRemoteIndicator != null) {
            mRemoteIndicator.setCallback(null);
            unscheduleDrawable(mRemoteIndicator);
    /**
     * Gets the media route types for filtering the routes that the user can
     * select using the media route chooser dialog.
     *
     * @return The route types.
     */
    public int getRouteTypes() {
        return mRouteTypes;
    }
        mRemoteIndicator = d;
        if (d != null) {
            d.setCallback(this);
            d.setState(getDrawableState());
            d.setVisible(getVisibility() == VISIBLE, false);

    /**
     * Sets the types of routes that will be shown in the media route chooser dialog
     * launched by this button.
     *
     * @param types The route types to match.
     */
    public void setRouteTypes(int types) {
        if (mRouteTypes != types) {
            if (mAttachedToWindow && mRouteTypes != 0) {
                mRouter.removeCallback(mCallback);
            }

        refreshDrawableState();
            mRouteTypes = types;

            if (mAttachedToWindow && types != 0) {
                mRouter.addCallback(types, mCallback,
                        MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
            }

    @Override
    public boolean performClick() {
        // Send the appropriate accessibility events and call listeners
        boolean handled = super.performClick();
        if (!handled) {
            playSoundEffect(SoundEffectConstants.CLICK);
            refreshRoute();
        }
    }

        if (mToggleMode) {
            if (mRemoteActive) {
                mRouter.selectRouteInt(mRouteTypes, mRouter.getDefaultRoute(), true);
            } else {
                final int N = mRouter.getRouteCount();
                for (int i = 0; i < N; i++) {
                    final RouteInfo route = mRouter.getRouteAt(i);
                    if ((route.getSupportedTypes() & mRouteTypes) != 0 &&
                            route != mRouter.getDefaultRoute()) {
                        mRouter.selectRouteInt(mRouteTypes, route, true);
    public void setExtendedSettingsClickListener(OnClickListener listener) {
        mExtendedSettingsClickListener = listener;
    }

    /**
     * Show the route chooser or controller dialog.
     * <p>
     * If the default route is selected or if the currently selected route does
     * not match the {@link #getRouteTypes route types}, then shows the route chooser dialog.
     * Otherwise, shows the route controller dialog to offer the user
     * a choice to disconnect from the route or perform other control actions
     * such as setting the route's volume.
     * </p><p>
     * This will attach a {@link DialogFragment} to the containing Activity.
     * </p>
     */
    public void showDialog() {
        showDialogInternal();
    }

    boolean showDialogInternal() {
        if (!mAttachedToWindow) {
            return false;
        }
        } else {
            showDialog();

        DialogFragment f = MediaRouteDialogPresenter.showDialogFragment(getActivity(),
                mRouteTypes, mExtendedSettingsClickListener);
        return f != null;
    }

        return handled;
    private Activity getActivity() {
        // Gross way of unwrapping the Activity so we can get the FragmentManager
        Context context = getContext();
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                return (Activity)context;
            }
            context = ((ContextWrapper)context).getBaseContext();
        }
        throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
    }

    /**
     * Sets whether to enable showing a toast with the content descriptor of the
     * button when the button is long pressed.
     */
    void setCheatSheetEnabled(boolean enable) {
        mCheatSheetEnabled = enable;
    }

    @Override
    public boolean performClick() {
        // Send the appropriate accessibility events and call listeners
        boolean handled = super.performClick();
        if (!handled) {
            playSoundEffect(SoundEffectConstants.CLICK);
        }
        return showDialogInternal() || handled;
    }

    @Override
    public boolean performLongClick() {
        if (super.performLongClick()) {
@@ -183,87 +228,9 @@ public class MediaRouteButton extends View {
        }
        cheatSheet.show();
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);

        return true;
    }

    public void setRouteTypes(int types) {
        if (types == mRouteTypes) {
            // Already registered; nothing to do.
            return;
        }

        if (mAttachedToWindow && mRouteTypes != 0) {
            mRouter.removeCallback(mRouterCallback);
        }

        mRouteTypes = types;

        if (mAttachedToWindow) {
            updateRouteInfo();
            mRouter.addCallback(types, mRouterCallback,
                    MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
        }
    }

    private void updateRouteInfo() {
        updateRemoteIndicator();
        updateRouteCount();
    }

    public int getRouteTypes() {
        return mRouteTypes;
    }

    void updateRemoteIndicator() {
        final RouteInfo selected = mRouter.getSelectedRoute(mRouteTypes);
        final boolean isRemote = selected != mRouter.getDefaultRoute();
        final boolean isConnecting = selected != null && selected.isConnecting();

        boolean needsRefresh = false;
        if (mRemoteActive != isRemote) {
            mRemoteActive = isRemote;
            needsRefresh = true;
        }
        if (mIsConnecting != isConnecting) {
            mIsConnecting = isConnecting;
            needsRefresh = true;
        }

        if (needsRefresh) {
            refreshDrawableState();
        }
    }

    void updateRouteCount() {
        final int N = mRouter.getRouteCount();
        int count = 0;
        boolean scanRequired = false;
        for (int i = 0; i < N; i++) {
            final RouteInfo route = mRouter.getRouteAt(i);
            final int routeTypes = route.getSupportedTypes();
            if ((routeTypes & mRouteTypes) != 0) {
                if (route instanceof RouteGroup) {
                    count += ((RouteGroup) route).getRouteCount();
                } else {
                    count++;
                }
                if (((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO
                        | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) != 0) {
                    scanRequired = true;
                }
            }
        }

        setEnabled(count != 0);

        // Only allow toggling if we have more than just user routes.
        // Don't toggle if we support video or remote display routes, we may have to
        // let the dialog scan.
        mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0
                && !scanRequired;
    }

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
@@ -291,6 +258,21 @@ public class MediaRouteButton extends View {
        }
    }

    private void setRemoteIndicatorDrawable(Drawable d) {
        if (mRemoteIndicator != null) {
            mRemoteIndicator.setCallback(null);
            unscheduleDrawable(mRemoteIndicator);
        }
        mRemoteIndicator = d;
        if (d != null) {
            d.setCallback(this);
            d.setState(getDrawableState());
            d.setVisible(getVisibility() == VISIBLE, false);
        }

        refreshDrawableState();
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return super.verifyDrawable(who) || who == mRemoteIndicator;
@@ -299,12 +281,16 @@ public class MediaRouteButton extends View {
    @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();
        if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState();

        if (mRemoteIndicator != null) {
            mRemoteIndicator.jumpToCurrentState();
        }
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);

        if (mRemoteIndicator != null) {
            mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
        }
@@ -313,20 +299,22 @@ public class MediaRouteButton extends View {
    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        mAttachedToWindow = true;
        if (mRouteTypes != 0) {
            mRouter.addCallback(mRouteTypes, mRouterCallback,
            mRouter.addCallback(mRouteTypes, mCallback,
                    MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
            updateRouteInfo();
        }
        refreshRoute();
    }

    @Override
    public void onDetachedFromWindow() {
        mAttachedToWindow = false;
        if (mRouteTypes != 0) {
            mRouter.removeCallback(mRouterCallback);
            mRouter.removeCallback(mCallback);
        }
        mAttachedToWindow = false;

        super.onDetachedFromWindow();
    }

@@ -389,93 +377,71 @@ public class MediaRouteButton extends View {
        final int drawLeft = left + (right - left - drawWidth) / 2;
        final int drawTop = top + (bottom - top - drawHeight) / 2;

        mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight);
        mRemoteIndicator.setBounds(drawLeft, drawTop,
                drawLeft + drawWidth, drawTop + drawHeight);
        mRemoteIndicator.draw(canvas);
    }

    public void setExtendedSettingsClickListener(OnClickListener listener) {
        mExtendedSettingsClickListener = listener;
        if (mDialogFragment != null) {
            mDialogFragment.setExtendedSettingsClickListener(listener);
        }
    }
    private void refreshRoute() {
        if (mAttachedToWindow) {
            final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
            final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes);
            final boolean isConnecting = isRemote && route.isConnecting();

    /**
     * Asynchronously show the route chooser dialog.
     * This will attach a {@link DialogFragment} to the containing Activity.
     */
    public void showDialog() {
        final FragmentManager fm = getActivity().getFragmentManager();
        if (mDialogFragment == null) {
            // See if one is already attached to this activity.
            mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag(
                    MediaRouteChooserDialogFragment.FRAGMENT_TAG);
            boolean needsRefresh = false;
            if (mRemoteActive != isRemote) {
                mRemoteActive = isRemote;
                needsRefresh = true;
            }
        if (mDialogFragment != null) {
            Log.w(TAG, "showDialog(): Already showing!");
            return;
            if (mIsConnecting != isConnecting) {
                mIsConnecting = isConnecting;
                needsRefresh = true;
            }

        mDialogFragment = new MediaRouteChooserDialogFragment();
        mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener);
        mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() {
            @Override
            public void onDetached(MediaRouteChooserDialogFragment detachedFragment) {
                mDialogFragment = null;
            }
        });
        mDialogFragment.setRouteTypes(mRouteTypes);
        mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG);
            if (needsRefresh) {
                refreshDrawableState();
            }

    private Activity getActivity() {
        // Gross way of unwrapping the Activity so we can get the FragmentManager
        Context context = getContext();
        while (context instanceof ContextWrapper && !(context instanceof Activity)) {
            context = ((ContextWrapper) context).getBaseContext();
        }
        if (!(context instanceof Activity)) {
            throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
            setEnabled(mRouter.isRouteAvailable(mRouteTypes,
                    MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
        }

        return (Activity) context;
    }

    private class MediaRouteCallback extends MediaRouter.SimpleCallback {
    private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
        @Override
        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
            updateRemoteIndicator();
        public void onRouteAdded(MediaRouter router, RouteInfo info) {
            refreshRoute();
        }

        @Override
        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
            updateRemoteIndicator();
        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
            refreshRoute();
        }

        @Override
        public void onRouteChanged(MediaRouter router, RouteInfo info) {
            updateRemoteIndicator();
            refreshRoute();
        }

        @Override
        public void onRouteAdded(MediaRouter router, RouteInfo info) {
            updateRouteCount();
        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
            refreshRoute();
        }

        @Override
        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
            updateRouteCount();
        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
            refreshRoute();
        }

        @Override
        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
                int index) {
            updateRouteCount();
            refreshRoute();
        }

        @Override
        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
            updateRouteCount();
            refreshRoute();
        }
    }
}
+272 −0

File added.

Preview size limit exceeded, changes collapsed.

+49 −638

File changed.

Preview size limit exceeded, changes collapsed.

+318 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading