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