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