419 lines
14 KiB
Java
419 lines
14 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
* <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();
|
|
}
|
|
}
|
|
}
|