blob: 61e63d1b30de5645b50a2987c4582d68cc063ec0 [file] [log] [blame]
Jeff Brown0abd3a62013-11-09 17:48:23 -08001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.app;
18
19import com.android.internal.R;
20
Donghyun Cho86165e32016-12-23 00:50:57 +090021import android.app.AlertDialog;
Jeff Brown0abd3a62013-11-09 17:48:23 -080022import android.app.MediaRouteActionProvider;
23import android.app.MediaRouteButton;
24import android.content.Context;
Donghyun Cho86165e32016-12-23 00:50:57 +090025import android.content.DialogInterface;
26import android.content.res.Resources;
27import android.content.res.TypedArray;
28import android.graphics.drawable.AnimationDrawable;
Jeff Brown0abd3a62013-11-09 17:48:23 -080029import android.graphics.drawable.Drawable;
Donghyun Cho86165e32016-12-23 00:50:57 +090030import android.graphics.drawable.StateListDrawable;
Jeff Brown0abd3a62013-11-09 17:48:23 -080031import android.media.MediaRouter;
32import android.media.MediaRouter.RouteGroup;
33import android.media.MediaRouter.RouteInfo;
34import android.os.Bundle;
Donghyun Cho86165e32016-12-23 00:50:57 +090035import android.util.TypedValue;
Jeff Brown0abd3a62013-11-09 17:48:23 -080036import android.view.KeyEvent;
37import android.view.View;
Jeff Brown0abd3a62013-11-09 17:48:23 -080038import android.widget.FrameLayout;
39import android.widget.LinearLayout;
40import android.widget.SeekBar;
41
42/**
43 * This class implements the route controller dialog for {@link MediaRouter}.
44 * <p>
45 * This dialog allows the user to control or disconnect from the currently selected route.
46 * </p>
47 *
48 * @see MediaRouteButton
49 * @see MediaRouteActionProvider
50 *
51 * TODO: Move this back into the API, as in the support library media router.
52 */
Donghyun Cho86165e32016-12-23 00:50:57 +090053public class MediaRouteControllerDialog extends AlertDialog {
Jeff Brown0abd3a62013-11-09 17:48:23 -080054 // Time to wait before updating the volume when the user lets go of the seek bar
55 // to allow the route provider time to propagate the change and publish a new
56 // route descriptor.
57 private static final int VOLUME_UPDATE_DELAY_MILLIS = 250;
58
59 private final MediaRouter mRouter;
60 private final MediaRouterCallback mCallback;
61 private final MediaRouter.RouteInfo mRoute;
62
63 private boolean mCreated;
Donghyun Cho86165e32016-12-23 00:50:57 +090064 private Drawable mMediaRouteButtonDrawable;
65 private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled };
66 private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled };
Jeff Brown0abd3a62013-11-09 17:48:23 -080067 private Drawable mCurrentIconDrawable;
68
69 private boolean mVolumeControlEnabled = true;
70 private LinearLayout mVolumeLayout;
71 private SeekBar mVolumeSlider;
72 private boolean mVolumeSliderTouched;
73
74 private View mControlView;
yangrend9125cf2017-06-28 15:39:50 -070075 private boolean mAttachedToWindow;
Jeff Brown0abd3a62013-11-09 17:48:23 -080076
Jeff Brown0abd3a62013-11-09 17:48:23 -080077 public MediaRouteControllerDialog(Context context, int theme) {
78 super(context, theme);
79
80 mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
81 mCallback = new MediaRouterCallback();
82 mRoute = mRouter.getSelectedRoute();
83 }
84
85 /**
86 * Gets the route that this dialog is controlling.
87 */
88 public MediaRouter.RouteInfo getRoute() {
89 return mRoute;
90 }
91
92 /**
93 * Provides the subclass an opportunity to create a view that will
94 * be included within the body of the dialog to offer additional media controls
95 * for the currently playing content.
96 *
97 * @param savedInstanceState The dialog's saved instance state.
98 * @return The media control view, or null if none.
99 */
100 public View onCreateMediaControlView(Bundle savedInstanceState) {
101 return null;
102 }
103
104 /**
105 * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
106 *
107 * @return The media control view, or null if none.
108 */
109 public View getMediaControlView() {
110 return mControlView;
111 }
112
113 /**
114 * Sets whether to enable the volume slider and volume control using the volume keys
115 * when the route supports it.
116 * <p>
117 * The default value is true.
118 * </p>
119 */
120 public void setVolumeControlEnabled(boolean enable) {
121 if (mVolumeControlEnabled != enable) {
122 mVolumeControlEnabled = enable;
123 if (mCreated) {
124 updateVolume();
125 }
126 }
127 }
128
129 /**
130 * Returns whether to enable the volume slider and volume control using the volume keys
131 * when the route supports it.
132 */
133 public boolean isVolumeControlEnabled() {
134 return mVolumeControlEnabled;
135 }
136
137 @Override
138 protected void onCreate(Bundle savedInstanceState) {
Donghyun Cho86165e32016-12-23 00:50:57 +0900139 setTitle(mRoute.getName());
140 Resources res = getContext().getResources();
141 setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect),
142 new OnClickListener() {
143 @Override
144 public void onClick(DialogInterface dialogInterface, int id) {
145 if (mRoute.isSelected()) {
Sungsoo Lim59579ce2017-07-14 10:28:51 -0700146 if (mRoute.isBluetooth()) {
147 mRouter.getDefaultRoute().select();
148 } else {
149 mRouter.getFallbackRoute().select();
150 }
Donghyun Cho86165e32016-12-23 00:50:57 +0900151 }
152 dismiss();
153 }
154 });
155 View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null);
156 setView(customView, 0, 0, 0, 0);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800157 super.onCreate(savedInstanceState);
158
Donghyun Cho86165e32016-12-23 00:50:57 +0900159 View customPanelView = getWindow().findViewById(R.id.customPanel);
160 if (customPanelView != null) {
161 customPanelView.setMinimumHeight(0);
162 }
163 mVolumeLayout = (LinearLayout) customView.findViewById(R.id.media_route_volume_layout);
164 mVolumeSlider = (SeekBar) customView.findViewById(R.id.media_route_volume_slider);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800165 mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
166 private final Runnable mStopTrackingTouch = new Runnable() {
167 @Override
168 public void run() {
169 if (mVolumeSliderTouched) {
170 mVolumeSliderTouched = false;
171 updateVolume();
172 }
173 }
174 };
175
176 @Override
177 public void onStartTrackingTouch(SeekBar seekBar) {
178 if (mVolumeSliderTouched) {
179 mVolumeSlider.removeCallbacks(mStopTrackingTouch);
180 } else {
181 mVolumeSliderTouched = true;
182 }
183 }
184
185 @Override
186 public void onStopTrackingTouch(SeekBar seekBar) {
187 // Defer resetting mVolumeSliderTouched to allow the media route provider
188 // a little time to settle into its new state and publish the final
189 // volume update.
190 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
191 }
192
193 @Override
194 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
195 if (fromUser) {
196 mRoute.requestSetVolume(progress);
197 }
198 }
199 });
200
Donghyun Cho86165e32016-12-23 00:50:57 +0900201 mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800202 mCreated = true;
203 if (update()) {
204 mControlView = onCreateMediaControlView(savedInstanceState);
205 FrameLayout controlFrame =
Donghyun Cho86165e32016-12-23 00:50:57 +0900206 (FrameLayout) customView.findViewById(R.id.media_route_control_frame);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800207 if (mControlView != null) {
208 controlFrame.addView(mControlView);
209 controlFrame.setVisibility(View.VISIBLE);
210 } else {
211 controlFrame.setVisibility(View.GONE);
212 }
213 }
214 }
215
Jeff Brown0abd3a62013-11-09 17:48:23 -0800216 @Override
217 public void onAttachedToWindow() {
218 super.onAttachedToWindow();
yangrend9125cf2017-06-28 15:39:50 -0700219 mAttachedToWindow = true;
Jeff Brown0abd3a62013-11-09 17:48:23 -0800220
221 mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
222 update();
223 }
224
225 @Override
226 public void onDetachedFromWindow() {
227 mRouter.removeCallback(mCallback);
yangrend9125cf2017-06-28 15:39:50 -0700228 mAttachedToWindow = false;
Jeff Brown0abd3a62013-11-09 17:48:23 -0800229
230 super.onDetachedFromWindow();
231 }
232
233 @Override
234 public boolean onKeyDown(int keyCode, KeyEvent event) {
235 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
236 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
237 mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
238 return true;
239 }
240 return super.onKeyDown(keyCode, event);
241 }
242
243 @Override
244 public boolean onKeyUp(int keyCode, KeyEvent event) {
245 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
246 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
247 return true;
248 }
249 return super.onKeyUp(keyCode, event);
250 }
251
252 private boolean update() {
253 if (!mRoute.isSelected() || mRoute.isDefault()) {
254 dismiss();
255 return false;
256 }
257
258 setTitle(mRoute.getName());
259 updateVolume();
260
261 Drawable icon = getIconDrawable();
262 if (icon != mCurrentIconDrawable) {
263 mCurrentIconDrawable = icon;
Donghyun Cho86165e32016-12-23 00:50:57 +0900264 if (icon instanceof AnimationDrawable) {
265 AnimationDrawable animDrawable = (AnimationDrawable) icon;
yangrend9125cf2017-06-28 15:39:50 -0700266 if (!mAttachedToWindow && !mRoute.isConnecting()) {
267 // When the route is already connected before the view is attached, show the
268 // last frame of the connected animation immediately.
269 if (animDrawable.isRunning()) {
270 animDrawable.stop();
271 }
272 icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1);
273 } else if (!animDrawable.isRunning()) {
Donghyun Cho86165e32016-12-23 00:50:57 +0900274 animDrawable.start();
275 }
276 }
277 setIcon(icon);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800278 }
279 return true;
280 }
281
Donghyun Cho86165e32016-12-23 00:50:57 +0900282 private Drawable obtainMediaRouteButtonDrawable() {
283 Context context = getContext();
284 TypedValue value = new TypedValue();
285 if (!context.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) {
286 return null;
287 }
288 int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable };
289 TypedArray a = context.obtainStyledAttributes(value.data, drawableAttrs);
290 Drawable drawable = a.getDrawable(0);
291 a.recycle();
292 return drawable;
293 }
294
Jeff Brown0abd3a62013-11-09 17:48:23 -0800295 private Drawable getIconDrawable() {
Donghyun Cho86165e32016-12-23 00:50:57 +0900296 if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) {
297 return mMediaRouteButtonDrawable;
298 } else if (mRoute.isConnecting()) {
299 StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
300 stateListDrawable.setState(mMediaRouteConnectingState);
301 return stateListDrawable.getCurrent();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800302 } else {
Donghyun Cho86165e32016-12-23 00:50:57 +0900303 StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
304 stateListDrawable.setState(mMediaRouteOnState);
305 return stateListDrawable.getCurrent();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800306 }
307 }
308
309 private void updateVolume() {
310 if (!mVolumeSliderTouched) {
311 if (isVolumeControlAvailable()) {
312 mVolumeLayout.setVisibility(View.VISIBLE);
313 mVolumeSlider.setMax(mRoute.getVolumeMax());
314 mVolumeSlider.setProgress(mRoute.getVolume());
315 } else {
316 mVolumeLayout.setVisibility(View.GONE);
317 }
318 }
319 }
320
321 private boolean isVolumeControlAvailable() {
322 return mVolumeControlEnabled && mRoute.getVolumeHandling() ==
323 MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
324 }
325
326 private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
327 @Override
328 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
329 update();
330 }
331
332 @Override
333 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
334 update();
335 }
336
337 @Override
338 public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
339 if (route == mRoute) {
340 updateVolume();
341 }
342 }
343
344 @Override
345 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
346 int index) {
347 update();
348 }
349
350 @Override
351 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
352 update();
353 }
354 }
355}