1 /*
2  * Copyright (C) 2022 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 com.google.android.car.kitchensink.media;
18 
19 import static android.R.layout.simple_spinner_dropdown_item;
20 import static android.R.layout.simple_spinner_item;
21 import static android.car.settings.CarSettings.Secure.KEY_DRIVER_ALLOWED_TO_CONTROL_MEDIA;
22 
23 import static androidx.core.content.IntentCompat.EXTRA_START_PLAYBACK;
24 
25 import static com.google.android.car.kitchensink.KitchenSinkActivity.DUMP_ARG_CMD;
26 import static com.google.android.car.kitchensink.media.MediaBrowserProxyService.MEDIA_BROWSER_SERVICE_COMPONENT_KEY;
27 
28 import android.annotation.UserIdInt;
29 import android.app.ActivityManager;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.media.MediaMetadata;
34 import android.media.browse.MediaBrowser;
35 import android.media.session.MediaController;
36 import android.media.session.PlaybackState;
37 import android.media.session.PlaybackState.State;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.Process;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.provider.Settings;
44 import android.util.Log;
45 import android.util.SparseArray;
46 import android.view.LayoutInflater;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.widget.AdapterView;
50 import android.widget.ArrayAdapter;
51 import android.widget.Button;
52 import android.widget.CheckBox;
53 import android.widget.Spinner;
54 import android.widget.TextView;
55 import android.widget.Toast;
56 
57 import androidx.annotation.Nullable;
58 import androidx.fragment.app.Fragment;
59 
60 import com.google.android.car.kitchensink.R;
61 import com.google.common.base.MoreObjects;
62 import com.google.common.collect.ImmutableMap;
63 
64 import java.io.FileDescriptor;
65 import java.io.PrintWriter;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Iterator;
69 import java.util.Set;
70 
71 /**
72  * Multi-user multi-display media demo.
73  *
74  * <p>This is a reference implementation to show that driver can start a YouTube video on
75  * a passenger screen and control the media session.
76  */
77 public final class MultidisplayMediaFragment extends Fragment {
78 
79     private static final String TAG = MultidisplayMediaFragment.class.getSimpleName();
80 
81     private static final String CMD_HELP = "help";
82     private static final String CMD_START = "start";
83     private static final String CMD_PAUSE = "pause";
84     private static final String CMD_RESUME = "resume";
85     private static final String CMD_PREV = "prev";
86     private static final String CMD_NEXT = "next";
87 
88     private static final String CONTROL_NOT_ALLOWED_MESSAGE_FORMAT = "User %d does not allow the "
89             + " driver to control their media session. Check the user's settings in the Settings.";
90     private static final String INSTALL_YOUTUBE_MESSAGE = "Cannot start a YouTube video."
91             + " Follow the instructions on go/mumd-media to install YouTube app.";
92     private static final String YOUTUBE_PACKAGE = "com.google.android.youtube";
93     // YouTube Activity class to play a video.
94     private static final String YOUTUBE_ACTIVITY_CLASS = YOUTUBE_PACKAGE + ".UrlActivity";
95     // YouTube MediaBrowserService class.
96     private static final String YOUTUBE_MEDIA_BROWSER_SERVICE_CLASS =
97             "com.google.android.apps.youtube.app.extensions.mediabrowser.impl"
98                     + ".MainAppMediaBrowserService";
99     private static final String FORCE_FULLSCREEN = "force_fullscreen";
100     private static final String YOUTUBE_WATCH_URL_BASE = "https://www.youtube.com/watch?v=";
101 
102     private static final int MAX_NUM_USERS = 5;
103     private static final String PAUSE = "Pause";
104     private static final String RESUME = "Resume";
105 
106     // Pre-defined list of videos to show for the demo.
107     private static final ImmutableMap<String, String> VIDEOS = ImmutableMap.of(
108             "Baby Shark", "48XeLQOok_U",
109             "Bruno Mars - The Lazy Song", "fLexgOxsZu0",
110             "BTS - Butter", "WMweEpGlu_U",
111             "PSY - GANGNAM STYLE", "9bZkp7q19f0",
112             "Rick Astley - Never Gonna Give You Up", "dQw4w9WgXcQ");
113 
114     private final SparseArray<Context> mUserContexts = new SparseArray<>(MAX_NUM_USERS);
115     private final SparseArray<MediaBrowser> mMediaBrowsers = new SparseArray<>(MAX_NUM_USERS);
116     private final SparseArray<MediaController> mMediaControllers = new SparseArray<>(MAX_NUM_USERS);
117 
118     private UserManager mUserManager;
119 
120     private CheckBox mAllowDriverCheckBox;
121     private Spinner mUserSpinner;
122     private Spinner mVideoSpinner;
123     private Button mStartButton;
124     private Button mPauseResumeButton;
125     private Button mPrevButton;
126     private Button mNextButton;
127     private TextView mNowPlaying;
128     private int mSelectedUserId;
129 
130     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)131     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
132             @Nullable Bundle savedInstanceState) {
133         mUserManager = getContext().getSystemService(UserManager.class);
134 
135         View view = inflater.inflate(R.layout.multidisplay_media, container, false);
136         initUserSpinner(view);
137         initVideoSpinner(view);
138 
139         mAllowDriverCheckBox = view.findViewById(R.id.allow_driver_checkbox);
140         mAllowDriverCheckBox.setOnClickListener(this::onClickAllowDriverCheckbox);
141         mStartButton = view.findViewById(R.id.start);
142         mStartButton.setOnClickListener(this::onClickStart);
143         mPauseResumeButton = view.findViewById(R.id.pause_resume);
144         mPauseResumeButton.setOnClickListener(this::onClickPauseResume);
145         mPrevButton = view.findViewById(R.id.previous);
146         mPrevButton.setOnClickListener(this::onClickPrev);
147         mNextButton = view.findViewById(R.id.next);
148         mNextButton.setOnClickListener(this::onClickNext);
149         setMediaButtonsEnabled(false);
150 
151         mNowPlaying = view.findViewById(R.id.now_playing);
152 
153         // Connect to media session of the currently selected user, if there is one already running.
154         connectMediaBrowser();
155 
156         refreshUi();
157         return view;
158     }
159 
160     /**
161      * <p>Usage:
162      * To dump the current state:
163      * $ adb shell 'dumpsys activity com.google.android.car.kitchensink/.KitchenSinkActivity \
164      *      fragment "MD media"'
165      *
166      * To execute a command:
167      * $ adb shell 'dumpsys activity com.google.android.car.kitchensink/.KitchenSinkActivity \
168      *      fragment "MD media" cmd <command>'
169      *
170      * Supported commands: help, start, pause, resume, prev, next
171      */
172     @Override
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)173     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
174         Log.v(TAG, "dump(): " + Arrays.toString(args));
175 
176         if (args != null && args.length > 0 && args[0].equals(DUMP_ARG_CMD)) {
177             runCmd(writer, args);
178             return;
179         }
180 
181         writer.printf("%SmUserContext: %s\n", prefix, mUserContexts);
182         writer.printf("%smMediaBrowsers: %s\n", prefix, mMediaBrowsers);
183         writer.printf("%smMediaControllers: %s\n", prefix, mMediaControllers);
184 
185         writer.printf("%smUserSpinner selectedItem: %s\n", prefix, mUserSpinner.getSelectedItem());
186         writer.printf("%smSelectedUserId: %s\n", prefix, mSelectedUserId);
187         writer.printf("%smVideoSpinner selectedItem: %s\n",
188                 prefix, mVideoSpinner.getSelectedItem());
189         writer.printf("%smStartButton, enabled:%s\n", prefix, mStartButton.isEnabled());
190         writer.printf("%smNowPlaying text: %s\n", prefix, mNowPlaying.getText());
191         writer.printf("%smPauseResumeButton text: %s, enabled:%s\n",
192                 prefix, mPauseResumeButton.getText(), mPauseResumeButton.isEnabled());
193         writer.printf("%smPrevButton, enabled:%s\n", prefix, mPrevButton.isEnabled());
194         writer.printf("%smNextButton, enabled:%s\n", prefix, mNextButton.isEnabled());
195     }
196 
runCmd(PrintWriter writer, String[] args)197     private void runCmd(PrintWriter writer, String[] args) {
198         if (args.length < 2) {
199             writer.println("missing command\n");
200             return;
201         }
202         String cmd = args[1];
203         switch (cmd) {
204             case CMD_HELP:
205                 showHelp(writer);
206                 break;
207             case CMD_START:
208                 callOnClickIfEnabled(writer, mStartButton);
209                 break;
210             case CMD_PAUSE:
211                 if (verifyButtonText(writer, mPauseResumeButton, PAUSE)) {
212                     callOnClickIfEnabled(writer, mPauseResumeButton);
213                 }
214                 break;
215             case CMD_RESUME:
216                 if (verifyButtonText(writer, mPauseResumeButton, RESUME)) {
217                     callOnClickIfEnabled(writer, mPauseResumeButton);
218                 }
219                 break;
220             case CMD_PREV:
221                 callOnClickIfEnabled(writer, mPrevButton);
222                 break;
223             case CMD_NEXT:
224                 callOnClickIfEnabled(writer, mNextButton);
225                 break;
226             default:
227                 writer.printf("Invalid cmd: %s\n", Arrays.toString(args));
228                 showHelp(writer);
229         }
230     }
231 
showHelp(PrintWriter writer)232     private void showHelp(PrintWriter writer) {
233         writer.printf("Available commands:\n");
234         writer.printf("  %s: Shows this help message.\n\n", CMD_HELP);
235         writer.printf("  %s: Start the selected video for the selected user.\n\n", CMD_START);
236         writer.printf("  %s: Pause the current video.\n\n", CMD_PAUSE);
237         writer.printf("  %s: Resume the currently paused video.\n\n", CMD_RESUME);
238         writer.printf("  %s: Go back to the previous video.\n\n", CMD_PREV);
239         writer.printf("  %s: Skip to the next video.\n\n", CMD_NEXT);
240     }
241 
verifyButtonText(PrintWriter writer, Button button, String text)242     private boolean verifyButtonText(PrintWriter writer, Button button, String text) {
243         CharSequence buttonText = button.getText();
244         if (buttonText != null && text.equals(buttonText.toString())) {
245             return true;
246         }
247 
248         writer.printf("Cannot execute %s command. The button is currently set to %s.\n",
249                 text, buttonText);
250         return false;
251     }
252 
callOnClickIfEnabled(PrintWriter writer, Button button)253     private void callOnClickIfEnabled(PrintWriter writer, Button button) {
254         if (!button.isEnabled()) {
255             writer.printf("Cannot execute %s command. The button is currently disabled.\n",
256                     button.getText());
257             return;
258         }
259         try {
260             button.callOnClick();
261         } catch (Exception e) {
262             writer.printf("Failed to call onClick() for button %s:\n%s\n", button.getText(), e);
263         }
264     }
265 
refreshUi()266     private void refreshUi() {
267         // Set the checkbox according to the CarSettings value.
268         mAllowDriverCheckBox.setChecked(getDriverAllowedToControlMedia(mSelectedUserId));
269 
270         MediaController mediaController = mMediaControllers.get(mSelectedUserId);
271         int state = PlaybackState.STATE_NONE;
272         MediaMetadata metadata = null;
273         if (mediaController != null) {
274             metadata = mediaController.getMetadata();
275             if (mediaController.getPlaybackState() != null) {
276                 state = mediaController.getPlaybackState().getState();
277             }
278         }
279 
280         updateNowPlaying(metadata);
281         updateButtons(state);
282     }
283 
284     /** Updates the "Now Playing" media title. */
updateNowPlaying(MediaMetadata metadata)285     private void updateNowPlaying(MediaMetadata metadata) {
286         CharSequence title = null;
287         if (metadata != null && metadata.getDescription() != null) {
288             title = metadata.getDescription().getTitle();
289         }
290         mNowPlaying.setText("Now playing: " + MoreObjects.firstNonNull(title, ""));
291     }
292 
293     /** Updates the button names and enable/disable buttons based on media playback state. */
updateButtons(@tate int state)294     private void updateButtons(@State int state) {
295         boolean enabled = false;
296         if (state != PlaybackState.STATE_NONE) {
297             enabled = true;
298         }
299         setMediaButtonsEnabled(enabled);
300         mPauseResumeButton.setText(state == PlaybackState.STATE_PLAYING ? PAUSE : RESUME);
301     }
302 
setMediaButtonsEnabled(boolean enabled)303     private void setMediaButtonsEnabled(boolean enabled) {
304         mPauseResumeButton.setEnabled(enabled);
305         mPrevButton.setEnabled(enabled);
306         mNextButton.setEnabled(enabled);
307     }
308 
connectMediaBrowser()309     private void connectMediaBrowser() {
310         MediaBrowser mediaBrowser = getOrCreateMediaBrowser(mSelectedUserId);
311         if (mediaBrowser != null && !mediaBrowser.isConnected()) {
312             Log.d(TAG, "Connecting to media browser service for user " + mSelectedUserId);
313             mediaBrowser.connect();
314         }
315     }
316 
disconnectMediaBrowser()317     private void disconnectMediaBrowser() {
318         MediaBrowser mediaBrowser = mMediaBrowsers.get(mSelectedUserId);
319         if (mediaBrowser != null && mediaBrowser.isConnected()) {
320             Log.d(TAG, "Disconnecting to media browser service for user " + mSelectedUserId);
321             mediaBrowser.disconnect();
322             mMediaControllers.set(mSelectedUserId, null);
323         }
324     }
325 
326     /**
327      * Sets the value for car settings {@link KEY_DRIVER_ALLOWED_TO_CONTROL_MEDIA}.
328      *
329      * <p>In practice, setting this value should be only done by Car Settings app.
330      */
setDriverAllowedToControlMedia(@serIdInt int userId, boolean allow)331     private void setDriverAllowedToControlMedia(@UserIdInt int userId, boolean allow) {
332         Settings.Secure.putInt(getOrCreateUserContext(userId).getContentResolver(),
333                 KEY_DRIVER_ALLOWED_TO_CONTROL_MEDIA, allow ? 1 : 0);
334     }
335 
336     /**
337      * Gets the value for car settings {@link KEY_DRIVER_ALLOWED_TO_CONTROL_MEDIA}.
338      *
339      * <p>Driver's Media Control Center app will use this value to check if a user has allowed
340      * the driver to control their media sessions.
341      */
getDriverAllowedToControlMedia(@serIdInt int userId)342     private boolean getDriverAllowedToControlMedia(@UserIdInt int userId) {
343         return Settings.Secure.getInt(getOrCreateUserContext(userId).getContentResolver(),
344                 KEY_DRIVER_ALLOWED_TO_CONTROL_MEDIA, /* default= */ 0) != 0;
345     }
346 
checkControlAllowedAndShowToast(@serIdInt int userId)347     private boolean checkControlAllowedAndShowToast(@UserIdInt int userId) {
348         boolean allow = getDriverAllowedToControlMedia(userId);
349         if (!allow) {
350             Toast.makeText(getContext(), String.format(CONTROL_NOT_ALLOWED_MESSAGE_FORMAT, userId),
351                     Toast.LENGTH_LONG).show();
352         }
353 
354         return allow;
355     }
356 
onClickAllowDriverCheckbox(View view)357     private void onClickAllowDriverCheckbox(View view) {
358         boolean checked = mAllowDriverCheckBox.isChecked();
359 
360         setDriverAllowedToControlMedia(mSelectedUserId, checked);
361     }
362 
363     /** Sends a YouTube video to the currently selected passenger user. */
onClickStart(View view)364     private void onClickStart(View view) {
365         Log.d(TAG, "onClickStart() for user " + mSelectedUserId);
366         if (!checkControlAllowedAndShowToast(mSelectedUserId)) {
367             return;
368         }
369 
370         String video = (String) mVideoSpinner.getSelectedItem();
371         String videoId = VIDEOS.get(video);
372 
373         Uri uri = Uri.parse(YOUTUBE_WATCH_URL_BASE + videoId);
374         Log.i(TAG, "Playing youtube uri " + uri + " for user " + mSelectedUserId);
375 
376         Intent intent = createPlayIntent(uri);
377         Log.d(TAG, "Starting Activity with intent: " + intent);
378 
379         try {
380             getContext().startActivityAsUser(intent, UserHandle.of(mSelectedUserId));
381         } catch (Exception e) {
382             Log.e(TAG, "Failed to start video " + video + "for user " + mSelectedUserId, e);
383             Toast.makeText(getContext(), INSTALL_YOUTUBE_MESSAGE, Toast.LENGTH_LONG).show();
384             return;
385         }
386 
387         connectMediaBrowser();
388         mPauseResumeButton.setText(PAUSE);
389     }
390 
onClickPauseResume(View view)391     private void onClickPauseResume(View view) {
392         Log.d(TAG, "onClickPlayResume() for user " + mSelectedUserId);
393         if (!checkControlAllowedAndShowToast(mSelectedUserId)) {
394             return;
395         }
396 
397         MediaController mediaController = mMediaControllers.get(mSelectedUserId);
398         if (mediaController == null) {
399             Log.e(TAG, "mediaController is null for user " + mSelectedUserId);
400             return;
401         }
402         MediaController.TransportControls transportControls =
403                 mediaController.getTransportControls();
404         if (transportControls == null) {
405             Log.e(TAG, "transport control is null for user " + mSelectedUserId);
406         }
407         PlaybackState playbackState = mediaController.getPlaybackState();
408         Log.d(TAG, "onClickPlayResume() playbackState: " + playbackState);
409 
410         int state = playbackState == null ? PlaybackState.STATE_NONE : playbackState.getState();
411         if (state == PlaybackState.STATE_PLAYING) {
412             transportControls.pause();
413         } else {
414             transportControls.play();
415         }
416     }
417 
onClickNext(View view)418     private void onClickNext(View view) {
419         Log.d(TAG, "onClickNext() for user " + mSelectedUserId);
420         if (!checkControlAllowedAndShowToast(mSelectedUserId)) {
421             return;
422         }
423 
424         MediaController mediaController = mMediaControllers.get(mSelectedUserId);
425         if (mediaController == null) {
426             Log.e(TAG, "mediaController is null for user " + mSelectedUserId);
427             return;
428         }
429         MediaController.TransportControls transportControls =
430                 mediaController.getTransportControls();
431         if (transportControls == null) {
432             Log.e(TAG, "transport control is null for user " + mSelectedUserId);
433         }
434 
435         transportControls.skipToNext();
436     }
437 
onClickPrev(View view)438     private void onClickPrev(View view) {
439         Log.d(TAG, "onClickPrev() for user " + mSelectedUserId);
440         if (!checkControlAllowedAndShowToast(mSelectedUserId)) {
441             return;
442         }
443 
444         MediaController mediaController = mMediaControllers.get(mSelectedUserId);
445         if (mediaController == null) {
446             Log.e(TAG, "mediaController is null for user " + mSelectedUserId);
447             return;
448         }
449         MediaController.TransportControls transportControls =
450                 mediaController.getTransportControls();
451         if (transportControls == null) {
452             Log.e(TAG, "transport control is null for user " + mSelectedUserId);
453         }
454 
455         transportControls.skipToPrevious();
456     }
457 
458     /** Initializes the user spinner with the visible users, excluding the driver user. */
initUserSpinner(View view)459     private void initUserSpinner(View view) {
460         mUserSpinner = view.findViewById(R.id.user_spinner);
461 
462         int currentUserId = ActivityManager.getCurrentUser();
463         ArrayList<Integer> userIds = new ArrayList<>();
464         Set<UserHandle> visibleUsers = mUserManager.getVisibleUsers();
465         for (Iterator<UserHandle> iterator = visibleUsers.iterator(); iterator.hasNext(); ) {
466             UserHandle userHandle = iterator.next();
467             int userId = userHandle.getIdentifier();
468             if (userId == currentUserId) {
469                 // Skip the current user (driver).
470                 continue;
471             }
472             userIds.add(userHandle.getIdentifier());
473         }
474 
475         ArrayAdapter<Integer> adapter =
476                 new ArrayAdapter<>(getContext(), simple_spinner_item, userIds);
477         adapter.setDropDownViewResource(simple_spinner_dropdown_item);
478         mUserSpinner.setAdapter(adapter);
479         mUserSpinner.setOnItemSelectedListener(new UserSwitcher());
480 
481         mSelectedUserId = (Integer) mUserSpinner.getSelectedItem();
482     }
483 
initVideoSpinner(View view)484     private void initVideoSpinner(View view) {
485         mVideoSpinner = view.findViewById(R.id.video_spinner);
486 
487         ArrayAdapter<String> adapter =
488                 new ArrayAdapter<>(getContext(), simple_spinner_item,
489                         new ArrayList<>(VIDEOS.keySet()));
490         adapter.setDropDownViewResource(simple_spinner_dropdown_item);
491         mVideoSpinner.setAdapter(adapter);
492     }
493 
494     /**
495      * Creates a bundle that contains the actual MediaBrowserService component to pass to the proxy
496      * service, so that proxy can connect to it.
497      */
createBundleWithMediaBrowserServiceComponent()498     private static Bundle createBundleWithMediaBrowserServiceComponent() {
499         String mediaBrowserService = new ComponentName(
500                 YOUTUBE_PACKAGE, YOUTUBE_MEDIA_BROWSER_SERVICE_CLASS).flattenToString();
501         Bundle bundle = new Bundle();
502         bundle.putString(MEDIA_BROWSER_SERVICE_COMPONENT_KEY, mediaBrowserService);
503 
504         return bundle;
505     }
506 
createPlayIntent(Uri uri)507     private static Intent createPlayIntent(Uri uri) {
508         return new Intent(Intent.ACTION_VIEW, uri)
509                 .setClassName(YOUTUBE_PACKAGE, YOUTUBE_ACTIVITY_CLASS)
510                 // FLAG_ACTIVITY_CLEAR_TASK flag is added so that the video that the YouTube app
511                 // first started with can be played again. Without the flag, the intent will be
512                 // ignored if the uri is same as what the existing activity first started with.
513                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
514                 .putExtra(EXTRA_START_PLAYBACK, true)
515                 .putExtra(FORCE_FULLSCREEN, false);
516     }
517 
getOrCreateUserContext(@serIdInt int userId)518     private Context getOrCreateUserContext(@UserIdInt int userId) {
519         Context userContext = mUserContexts.get(userId);
520         if (userContext != null) {
521             return userContext;
522         }
523 
524         UserHandle userHandle = UserHandle.of(userId);
525         Context context = getContext();
526         if (!userHandle.equals(Process.myUserHandle())) {
527             try {
528                 context = context.createContextAsUser(userHandle, /* flags= */ 0);
529                 Log.d(TAG, "Successfully created a context as user " + userId);
530             } catch (Exception e) {
531                 Log.e(TAG, "createContextAsUser() failed for user " + userId);
532             }
533         }
534 
535         mUserContexts.set(userId, context);
536         return context;
537     }
538 
getOrCreateMediaBrowser(@serIdInt int userId)539     private MediaBrowser getOrCreateMediaBrowser(@UserIdInt int userId) {
540         MediaBrowser mediaBrowser = mMediaBrowsers.get(userId);
541         if (mediaBrowser != null) {
542             Log.d(TAG, "User media browser already created for user " + userId);
543             return mediaBrowser;
544         }
545 
546         Context userContext = getOrCreateUserContext(userId);
547         mediaBrowser = new MediaBrowser(userContext,
548                 new ComponentName(userContext, MediaBrowserProxyService.class),
549                 new ConnectionCallback(userId), createBundleWithMediaBrowserServiceComponent());
550         Log.d(TAG, "A MediaBrowser " + mediaBrowser + " created for user "
551                 + userContext.getUserId());
552 
553         mMediaBrowsers.set(userId, mediaBrowser);
554         return mediaBrowser;
555     }
556 
557     private final class ConnectionCallback extends MediaBrowser.ConnectionCallback {
558 
559         private final int mUserId;
560 
ConnectionCallback(@serIdInt int userId)561         ConnectionCallback(@UserIdInt int userId) {
562             mUserId = userId;
563         }
564 
565         @Override
onConnected()566         public void onConnected() {
567             Log.d(TAG, "onConnected(): user " + mUserId);
568             MediaBrowser mediaBrowser = getOrCreateMediaBrowser(mUserId);
569             Log.d(TAG, "onConnected(): user " + mUserId + ", session token "
570                     + mediaBrowser.getSessionToken());
571             if (mediaBrowser.getSessionToken() == null) {
572                 throw new IllegalArgumentException("No Session token");
573             }
574 
575             MediaController mediaController = new MediaController(
576                     getOrCreateUserContext(mUserId), mediaBrowser.getSessionToken());
577             PlaybackState playbackState = mediaController.getPlaybackState();
578             Log.d(TAG, "A MediaController " + mediaController + " created for user "
579                     + mUserId + ", playback state: " + playbackState);
580             mediaController.registerCallback(new SessionCallback(mUserId));
581             mMediaControllers.set(mUserId, mediaController);
582 
583             refreshUi();
584         }
585 
586         @Override
onConnectionFailed()587         public void onConnectionFailed() {
588             Log.d(TAG, "onConnectionFailed(): user " + mUserId);
589             setMediaButtonsEnabled(false);
590         }
591 
592         @Override
onConnectionSuspended()593         public void onConnectionSuspended() {
594             Log.d(TAG, "onConnectionSuspended(): user " + mUserId);
595             setMediaButtonsEnabled(false);
596             mMediaControllers.set(mUserId, null);
597         }
598     };
599 
600     private final class SessionCallback extends MediaController.Callback {
601 
602         private final int mUserId;
603 
SessionCallback(@serIdInt int userId)604         SessionCallback(@UserIdInt int userId) {
605             mUserId = userId;
606         }
607 
608         @Override
onSessionDestroyed()609         public void onSessionDestroyed() {
610             Log.d(TAG, "onSessionDestroyed(): user " + mUserId);
611             MediaBrowser mediaBrowser = mMediaBrowsers.get(mUserId);
612             if (mediaBrowser != null) {
613                 mediaBrowser.disconnect();
614             }
615             mMediaControllers.set(mUserId, null);
616         }
617 
618         @Override
onMetadataChanged(MediaMetadata metadata)619         public void onMetadataChanged(MediaMetadata metadata) {
620             Log.d(TAG, "onMetadataChanged(): user " + mUserId + " metadata " + metadata);
621             if (mSelectedUserId != mUserId) {
622                 return;
623             }
624             updateNowPlaying(metadata);
625         }
626 
627         @Override
onPlaybackStateChanged(PlaybackState playbackState)628         public void onPlaybackStateChanged(PlaybackState playbackState) {
629             Log.d(TAG, "onPlaybackStateChanged(): user " + mUserId
630                     + " playbackState " + playbackState);
631             if (mSelectedUserId != mUserId) {
632                 return;
633             }
634             updateButtons(playbackState == null
635                     ? PlaybackState.STATE_NONE : playbackState.getState());
636         }
637     }
638 
639     private final class UserSwitcher implements AdapterView.OnItemSelectedListener {
onItemSelected(AdapterView<?> parent, View view, int pos, long id)640         public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
641             int selectedUserId = (Integer) parent.getItemAtPosition(pos);
642             if (selectedUserId == mSelectedUserId) {
643                 return;
644             }
645             disconnectMediaBrowser();
646             Log.d(TAG, "Selected user: " + selectedUserId);
647             mSelectedUserId = selectedUserId;
648             connectMediaBrowser();
649 
650             refreshUi();
651         }
652 
onNothingSelected(AdapterView<?> parent)653         public void onNothingSelected(AdapterView<?> parent) {
654             // no op.
655         }
656     }
657 }
658