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