[go: nahoru, domu]

blob: 3d8a278053fa0384aaf755c71143b2613fb2c344 [file] [log] [blame]
Aurimas Liutikasac5fe7c2018-03-06 14:40:53 -08001/*
2 * Copyright 2018 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 androidx.mediarouter.app;
18
19import static androidx.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED;
20import static androidx.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING;
21
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Message;
29import android.os.SystemClock;
30import androidx.annotation.NonNull;
31import androidx.appcompat.app.AppCompatDialog;
32import androidx.mediarouter.media.MediaRouteSelector;
33import androidx.mediarouter.media.MediaRouter;
34import androidx.mediarouter.R;
35import android.text.TextUtils;
36import android.util.Log;
37import android.view.Gravity;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.AdapterView;
42import android.widget.ArrayAdapter;
43import android.widget.ImageView;
44import android.widget.ListView;
45import android.widget.TextView;
46
47import java.io.IOException;
48import java.io.InputStream;
49import java.util.ArrayList;
50import java.util.Collections;
51import java.util.Comparator;
52import java.util.List;
53
54/**
55 * This class implements the route chooser dialog for {@link MediaRouter}.
56 * <p>
57 * This dialog allows the user to choose a route that matches a given selector.
58 * </p>
59 *
60 * @see MediaRouteButton
61 * @see MediaRouteActionProvider
62 */
63public class MediaRouteChooserDialog extends AppCompatDialog {
64 static final String TAG = "MediaRouteChooserDialog";
65
66 // Do not update the route list immediately to avoid unnatural dialog change.
67 private static final long UPDATE_ROUTES_DELAY_MS = 300L;
68 static final int MSG_UPDATE_ROUTES = 1;
69
70 private final MediaRouter mRouter;
71 private final MediaRouterCallback mCallback;
72
73 private TextView mTitleView;
74 private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
75 private ArrayList<MediaRouter.RouteInfo> mRoutes;
76 private RouteAdapter mAdapter;
77 private ListView mListView;
78 private boolean mAttachedToWindow;
79 private long mLastUpdateTime;
80 private final Handler mHandler = new Handler() {
81 @Override
82 public void handleMessage(Message message) {
83 switch (message.what) {
84 case MSG_UPDATE_ROUTES:
85 updateRoutes((List<MediaRouter.RouteInfo>) message.obj);
86 break;
87 }
88 }
89 };
90
91 public MediaRouteChooserDialog(Context context) {
92 this(context, 0);
93 }
94
95 public MediaRouteChooserDialog(Context context, int theme) {
96 super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false),
97 MediaRouterThemeHelper.createThemedDialogStyle(context));
98 context = getContext();
99
100 mRouter = MediaRouter.getInstance(context);
101 mCallback = new MediaRouterCallback();
102 }
103
104 /**
105 * Gets the media route selector for filtering the routes that the user can select.
106 *
107 * @return The selector, never null.
108 */
109 @NonNull
110 public MediaRouteSelector getRouteSelector() {
111 return mSelector;
112 }
113
114 /**
115 * Sets the media route selector for filtering the routes that the user can select.
116 *
117 * @param selector The selector, must not be null.
118 */
119 public void setRouteSelector(@NonNull MediaRouteSelector selector) {
120 if (selector == null) {
121 throw new IllegalArgumentException("selector must not be null");
122 }
123
124 if (!mSelector.equals(selector)) {
125 mSelector = selector;
126
127 if (mAttachedToWindow) {
128 mRouter.removeCallback(mCallback);
129 mRouter.addCallback(selector, mCallback,
130 MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
131 }
132
133 refreshRoutes();
134 }
135 }
136
137 /**
138 * Called to filter the set of routes that should be included in the list.
139 * <p>
140 * The default implementation iterates over all routes in the provided list and
141 * removes those for which {@link #onFilterRoute} returns false.
142 * </p>
143 *
144 * @param routes The list of routes to filter in-place, never null.
145 */
146 public void onFilterRoutes(@NonNull List<MediaRouter.RouteInfo> routes) {
147 for (int i = routes.size(); i-- > 0; ) {
148 if (!onFilterRoute(routes.get(i))) {
149 routes.remove(i);
150 }
151 }
152 }
153
154 /**
155 * Returns true if the route should be included in the list.
156 * <p>
157 * The default implementation returns true for enabled non-default routes that
158 * match the selector. Subclasses can override this method to filter routes
159 * differently.
160 * </p>
161 *
162 * @param route The route to consider, never null.
163 * @return True if the route should be included in the chooser dialog.
164 */
165 public boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) {
166 return !route.isDefaultOrBluetooth() && route.isEnabled()
167 && route.matchesSelector(mSelector);
168 }
169
170 @Override
171 public void setTitle(CharSequence title) {
172 mTitleView.setText(title);
173 }
174
175 @Override
176 public void setTitle(int titleId) {
177 mTitleView.setText(titleId);
178 }
179
180 @Override
181 protected void onCreate(Bundle savedInstanceState) {
182 super.onCreate(savedInstanceState);
183
184 setContentView(R.layout.mr_chooser_dialog);
185
186 mRoutes = new ArrayList<>();
187 mAdapter = new RouteAdapter(getContext(), mRoutes);
188 mListView = (ListView)findViewById(R.id.mr_chooser_list);
189 mListView.setAdapter(mAdapter);
190 mListView.setOnItemClickListener(mAdapter);
191 mListView.setEmptyView(findViewById(android.R.id.empty));
192 mTitleView = findViewById(R.id.mr_chooser_title);
193
194 updateLayout();
195 }
196
197 /**
198 * Sets the width of the dialog. Also called when configuration changes.
199 */
200 void updateLayout() {
201 getWindow().setLayout(MediaRouteDialogHelper.getDialogWidth(getContext()),
202 ViewGroup.LayoutParams.WRAP_CONTENT);
203 }
204
205 @Override
206 public void onAttachedToWindow() {
207 super.onAttachedToWindow();
208
209 mAttachedToWindow = true;
210 mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
211 refreshRoutes();
212 }
213
214 @Override
215 public void onDetachedFromWindow() {
216 mAttachedToWindow = false;
217 mRouter.removeCallback(mCallback);
218 mHandler.removeMessages(MSG_UPDATE_ROUTES);
219
220 super.onDetachedFromWindow();
221 }
222
223 /**
224 * Refreshes the list of routes that are shown in the chooser dialog.
225 */
226 public void refreshRoutes() {
227 if (mAttachedToWindow) {
228 ArrayList<MediaRouter.RouteInfo> routes = new ArrayList<>(mRouter.getRoutes());
229 onFilterRoutes(routes);
230 Collections.sort(routes, RouteComparator.sInstance);
231 if (SystemClock.uptimeMillis() - mLastUpdateTime >= UPDATE_ROUTES_DELAY_MS) {
232 updateRoutes(routes);
233 } else {
234 mHandler.removeMessages(MSG_UPDATE_ROUTES);
235 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ROUTES, routes),
236 mLastUpdateTime + UPDATE_ROUTES_DELAY_MS);
237 }
238 }
239 }
240
241 void updateRoutes(List<MediaRouter.RouteInfo> routes) {
242 mLastUpdateTime = SystemClock.uptimeMillis();
243 mRoutes.clear();
244 mRoutes.addAll(routes);
245 mAdapter.notifyDataSetChanged();
246 }
247
248 private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
249 implements ListView.OnItemClickListener {
250 private final LayoutInflater mInflater;
251 private final Drawable mDefaultIcon;
252 private final Drawable mTvIcon;
253 private final Drawable mSpeakerIcon;
254 private final Drawable mSpeakerGroupIcon;
255
256 public RouteAdapter(Context context, List<MediaRouter.RouteInfo> routes) {
257 super(context, 0, routes);
258 mInflater = LayoutInflater.from(context);
259 TypedArray styledAttributes = getContext().obtainStyledAttributes(new int[] {
260 R.attr.mediaRouteDefaultIconDrawable,
261 R.attr.mediaRouteTvIconDrawable,
262 R.attr.mediaRouteSpeakerIconDrawable,
263 R.attr.mediaRouteSpeakerGroupIconDrawable});
264 mDefaultIcon = styledAttributes.getDrawable(0);
265 mTvIcon = styledAttributes.getDrawable(1);
266 mSpeakerIcon = styledAttributes.getDrawable(2);
267 mSpeakerGroupIcon = styledAttributes.getDrawable(3);
268 styledAttributes.recycle();
269 }
270
271 @Override
272 public boolean areAllItemsEnabled() {
273 return false;
274 }
275
276 @Override
277 public boolean isEnabled(int position) {
278 return getItem(position).isEnabled();
279 }
280
281 @Override
282 public View getView(int position, View convertView, ViewGroup parent) {
283 View view = convertView;
284 if (view == null) {
285 view = mInflater.inflate(R.layout.mr_chooser_list_item, parent, false);
286 }
287
288 MediaRouter.RouteInfo route = getItem(position);
289 TextView text1 = (TextView) view.findViewById(R.id.mr_chooser_route_name);
290 TextView text2 = (TextView) view.findViewById(R.id.mr_chooser_route_desc);
291 text1.setText(route.getName());
292 String description = route.getDescription();
293 boolean isConnectedOrConnecting =
294 route.getConnectionState() == CONNECTION_STATE_CONNECTED
295 || route.getConnectionState() == CONNECTION_STATE_CONNECTING;
296 if (isConnectedOrConnecting && !TextUtils.isEmpty(description)) {
297 text1.setGravity(Gravity.BOTTOM);
298 text2.setVisibility(View.VISIBLE);
299 text2.setText(description);
300 } else {
301 text1.setGravity(Gravity.CENTER_VERTICAL);
302 text2.setVisibility(View.GONE);
303 text2.setText("");
304 }
305 view.setEnabled(route.isEnabled());
306
307 ImageView iconView = (ImageView) view.findViewById(R.id.mr_chooser_route_icon);
308 if (iconView != null) {
309 iconView.setImageDrawable(getIconDrawable(route));
310 }
311 return view;
312 }
313
314 @Override
315 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
316 MediaRouter.RouteInfo route = getItem(position);
317 if (route.isEnabled()) {
318 route.select();
319 dismiss();
320 }
321 }
322
323 private Drawable getIconDrawable(MediaRouter.RouteInfo route) {
324 Uri iconUri = route.getIconUri();
325 if (iconUri != null) {
326 try {
327 InputStream is = getContext().getContentResolver().openInputStream(iconUri);
328 Drawable drawable = Drawable.createFromStream(is, null);
329 if (drawable != null) {
330 return drawable;
331 }
332 } catch (IOException e) {
333 Log.w(TAG, "Failed to load " + iconUri, e);
334 // Falls back.
335 }
336 }
337 return getDefaultIconDrawable(route);
338 }
339
340 private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) {
341 // If the type of the receiver device is specified, use it.
342 switch (route.getDeviceType()) {
343 case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
344 return mTvIcon;
345 case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER:
346 return mSpeakerIcon;
347 }
348
349 // Otherwise, make the best guess based on other route information.
350 if (route instanceof MediaRouter.RouteGroup) {
351 // Only speakers can be grouped for now.
352 return mSpeakerGroupIcon;
353 }
354 return mDefaultIcon;
355 }
356 }
357
358 private final class MediaRouterCallback extends MediaRouter.Callback {
359 MediaRouterCallback() {
360 }
361
362 @Override
363 public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
364 refreshRoutes();
365 }
366
367 @Override
368 public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
369 refreshRoutes();
370 }
371
372 @Override
373 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
374 refreshRoutes();
375 }
376
377 @Override
378 public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
379 dismiss();
380 }
381 }
382
383 static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
384 public static final RouteComparator sInstance = new RouteComparator();
385
386 @Override
387 public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
388 return lhs.getName().compareToIgnoreCase(rhs.getName());
389 }
390 }
391}