/*
 * Copyright (C) 2012 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.app;

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

import android.annotation.NonNull;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaRouter;
import android.media.MediaRouter.RouteGroup;
import android.media.MediaRouter.RouteInfo;
import android.util.AttributeSet;
import android.view.SoundEffectConstants;
import android.view.View;

public class MediaRouteButton extends View {
    private final MediaRouter mRouter;
    private final MediaRouterCallback mCallback;

    private int mRouteTypes;

    private boolean mAttachedToWindow;

    private Drawable mRemoteIndicator;
    private boolean mRemoteActive;
    private boolean mIsConnecting;

    private int mMinWidth;
    private int mMinHeight;

    private OnClickListener mExtendedSettingsClickListener;

    // 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
    };

    public MediaRouteButton(Context context) {
        this(context, null);
    }

    public MediaRouteButton(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
    }

    public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MediaRouteButton(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

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

        final TypedArray a = context.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes);
        setRemoteIndicatorDrawable(a.getDrawable(
                com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
        mMinWidth = a.getDimensionPixelSize(
                com.android.internal.R.styleable.MediaRouteButton_minWidth, 0);
        mMinHeight = a.getDimensionPixelSize(
                com.android.internal.R.styleable.MediaRouteButton_minHeight, 0);
        final int routeTypes = a.getInteger(
                com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
                MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
        a.recycle();

        setClickable(true);

        setRouteTypes(routeTypes);
    }

    
Gets the media route types for filtering the routes that the user can select using the media route chooser dialog.
Returns:The route types.
/** * 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; }
Sets the types of routes that will be shown in the media route chooser dialog launched by this button.
Params:
  • types – The route types to match.
/** * 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); } mRouteTypes = types; if (mAttachedToWindow && types != 0) { mRouter.addCallback(types, mCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); } refreshRoute(); } } public void setExtendedSettingsClickListener(OnClickListener listener) { mExtendedSettingsClickListener = listener; }
Show the route chooser or controller dialog.

If the default route is selected or if the currently selected route does not match the 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.

This will attach a DialogFragment to the containing Activity.

/** * 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; } DialogFragment f = MediaRouteDialogPresenter.showDialogFragment(getActivity(), mRouteTypes, mExtendedSettingsClickListener); return f != null; } 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."); } @Override public void setContentDescription(CharSequence contentDescription) { super.setContentDescription(contentDescription); setTooltipText(contentDescription); } @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 protected int[] onCreateDrawableState(int extraSpace) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); // Technically we should be handling this more completely, but these // are implementation details here. Checked is used to express the connecting // drawable state and it's mutually exclusive with activated for the purposes // of state selection here. if (mIsConnecting) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } else if (mRemoteActive) { mergeDrawableStates(drawableState, ACTIVATED_STATE_SET); } return drawableState; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final Drawable remoteIndicator = mRemoteIndicator; if (remoteIndicator != null && remoteIndicator.isStateful() && remoteIndicator.setState(getDrawableState())) { invalidateDrawable(remoteIndicator); } } 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(@NonNull Drawable who) { return super.verifyDrawable(who) || who == mRemoteIndicator; } @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mRemoteIndicator != null) { mRemoteIndicator.jumpToCurrentState(); } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (mRemoteIndicator != null) { mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); } } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); mAttachedToWindow = true; if (mRouteTypes != 0) { mRouter.addCallback(mRouteTypes, mCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); } refreshRoute(); } @Override public void onDetachedFromWindow() { mAttachedToWindow = false; if (mRouteTypes != 0) { mRouter.removeCallback(mCallback); } super.onDetachedFromWindow(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int width = Math.max(mMinWidth, mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0); final int height = Math.max(mMinHeight, mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0); int measuredWidth; switch (widthMode) { case MeasureSpec.EXACTLY: measuredWidth = widthSize; break; case MeasureSpec.AT_MOST: measuredWidth = Math.min(widthSize, width); break; default: case MeasureSpec.UNSPECIFIED: measuredWidth = width; break; } int measuredHeight; switch (heightMode) { case MeasureSpec.EXACTLY: measuredHeight = heightSize; break; case MeasureSpec.AT_MOST: measuredHeight = Math.min(heightSize, height); break; default: case MeasureSpec.UNSPECIFIED: measuredHeight = height; break; } setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mRemoteIndicator == null) return; final int left = getPaddingLeft(); final int right = getWidth() - getPaddingRight(); final int top = getPaddingTop(); final int bottom = getHeight() - getPaddingBottom(); final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); 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.draw(canvas); } private void refreshRoute() { final MediaRouter.RouteInfo route = mRouter.getSelectedRoute(); final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes); final boolean isConnecting = isRemote && route.isConnecting(); boolean needsRefresh = false; if (mRemoteActive != isRemote) { mRemoteActive = isRemote; needsRefresh = true; } if (mIsConnecting != isConnecting) { mIsConnecting = isConnecting; needsRefresh = true; } if (needsRefresh) { refreshDrawableState(); } if (mAttachedToWindow) { setEnabled(mRouter.isRouteAvailable(mRouteTypes, MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE)); } if (mRemoteIndicator != null && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) { AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent(); if (mAttachedToWindow) { if ((needsRefresh || isConnecting) && !curDrawable.isRunning()) { curDrawable.start(); } } else if (isRemote && !isConnecting) { // When the route is already connected before the view is attached, show the last // frame of the connected animation immediately. if (curDrawable.isRunning()) { curDrawable.stop(); } curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1); } } } private final class MediaRouterCallback extends MediaRouter.SimpleCallback { @Override public void onRouteAdded(MediaRouter router, RouteInfo info) { refreshRoute(); } @Override public void onRouteRemoved(MediaRouter router, RouteInfo info) { refreshRoute(); } @Override public void onRouteChanged(MediaRouter router, RouteInfo info) { refreshRoute(); } @Override public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { refreshRoute(); } @Override public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { refreshRoute(); } @Override public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index) { refreshRoute(); } @Override public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { refreshRoute(); } } }