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