1 /* 2 * Copyright (C) 2020 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.android.systemui.media.dialog; 18 19 import static android.view.WindowInsets.Type.navigationBars; 20 import static android.view.WindowInsets.Type.statusBars; 21 22 import android.annotation.NonNull; 23 import android.app.WallpaperColors; 24 import android.bluetooth.BluetoothLeBroadcast; 25 import android.bluetooth.BluetoothLeBroadcastMetadata; 26 import android.content.Context; 27 import android.content.SharedPreferences; 28 import android.content.res.Configuration; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.ColorFilter; 32 import android.graphics.PixelFormat; 33 import android.graphics.PorterDuff; 34 import android.graphics.PorterDuffColorFilter; 35 import android.graphics.drawable.BitmapDrawable; 36 import android.graphics.drawable.Drawable; 37 import android.graphics.drawable.Icon; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.Gravity; 44 import android.view.LayoutInflater; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.view.ViewTreeObserver; 48 import android.view.Window; 49 import android.view.WindowInsets; 50 import android.view.WindowManager; 51 import android.widget.Button; 52 import android.widget.ImageView; 53 import android.widget.LinearLayout; 54 import android.widget.TextView; 55 56 import androidx.annotation.VisibleForTesting; 57 import androidx.core.graphics.drawable.IconCompat; 58 import androidx.recyclerview.widget.LinearLayoutManager; 59 import androidx.recyclerview.widget.RecyclerView; 60 61 import com.android.systemui.broadcast.BroadcastSender; 62 import com.android.systemui.res.R; 63 import com.android.systemui.statusbar.phone.SystemUIDialog; 64 65 import java.util.concurrent.Executor; 66 import java.util.concurrent.Executors; 67 68 /** 69 * Base dialog for media output UI 70 */ 71 public abstract class MediaOutputBaseDialog extends SystemUIDialog implements 72 MediaOutputController.Callback, Window.Callback { 73 74 private static final String TAG = "MediaOutputDialog"; 75 private static final String EMPTY_TITLE = " "; 76 private static final String PREF_NAME = "MediaOutputDialog"; 77 private static final String PREF_IS_LE_BROADCAST_FIRST_LAUNCH = "PrefIsLeBroadcastFirstLaunch"; 78 private static final boolean DEBUG = true; 79 private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; 80 81 protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 82 private final RecyclerView.LayoutManager mLayoutManager; 83 84 final Context mContext; 85 final MediaOutputController mMediaOutputController; 86 final BroadcastSender mBroadcastSender; 87 88 /** 89 * Signals whether the dialog should NOT show app-related metadata. 90 * 91 * <p>A metadata-less dialog hides the title, subtitle, and app icon in the header. 92 */ 93 private final boolean mIncludePlaybackAndAppMetadata; 94 95 @VisibleForTesting 96 View mDialogView; 97 private TextView mHeaderTitle; 98 private TextView mHeaderSubtitle; 99 private ImageView mHeaderIcon; 100 private ImageView mAppResourceIcon; 101 private ImageView mBroadcastIcon; 102 private RecyclerView mDevicesRecyclerView; 103 private LinearLayout mDeviceListLayout; 104 private LinearLayout mCastAppLayout; 105 private LinearLayout mMediaMetadataSectionLayout; 106 private Button mDoneButton; 107 private Button mStopButton; 108 private Button mAppButton; 109 private int mListMaxHeight; 110 private int mItemHeight; 111 private int mListPaddingTop; 112 private WallpaperColors mWallpaperColors; 113 private boolean mShouldLaunchLeBroadcastDialog; 114 private boolean mIsLeBroadcastCallbackRegistered; 115 private boolean mDismissing; 116 117 MediaOutputBaseAdapter mAdapter; 118 119 protected Executor mExecutor; 120 121 private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { 122 ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams(); 123 int totalItemsHeight = mAdapter.getItemCount() * mItemHeight 124 + mListPaddingTop; 125 int correctHeight = Math.min(totalItemsHeight, mListMaxHeight); 126 // Set max height for list 127 if (correctHeight != params.height) { 128 params.height = correctHeight; 129 mDeviceListLayout.setLayoutParams(params); 130 } 131 }; 132 133 private final BluetoothLeBroadcast.Callback mBroadcastCallback = 134 new BluetoothLeBroadcast.Callback() { 135 @Override 136 public void onBroadcastStarted(int reason, int broadcastId) { 137 if (DEBUG) { 138 Log.d(TAG, "onBroadcastStarted(), reason = " + reason 139 + ", broadcastId = " + broadcastId); 140 } 141 mMainThreadHandler.post(() -> handleLeBroadcastStarted()); 142 } 143 144 @Override 145 public void onBroadcastStartFailed(int reason) { 146 if (DEBUG) { 147 Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); 148 } 149 mMainThreadHandler.postDelayed(() -> handleLeBroadcastStartFailed(), 150 HANDLE_BROADCAST_FAILED_DELAY); 151 } 152 153 @Override 154 public void onBroadcastMetadataChanged(int broadcastId, 155 @NonNull BluetoothLeBroadcastMetadata metadata) { 156 if (DEBUG) { 157 Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId 158 + ", metadata = " + metadata); 159 } 160 mMainThreadHandler.post(() -> handleLeBroadcastMetadataChanged()); 161 } 162 163 @Override 164 public void onBroadcastStopped(int reason, int broadcastId) { 165 if (DEBUG) { 166 Log.d(TAG, "onBroadcastStopped(), reason = " + reason 167 + ", broadcastId = " + broadcastId); 168 } 169 mMainThreadHandler.post(() -> handleLeBroadcastStopped()); 170 } 171 172 @Override 173 public void onBroadcastStopFailed(int reason) { 174 if (DEBUG) { 175 Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); 176 } 177 mMainThreadHandler.post(() -> handleLeBroadcastStopFailed()); 178 } 179 180 @Override 181 public void onBroadcastUpdated(int reason, int broadcastId) { 182 if (DEBUG) { 183 Log.d(TAG, "onBroadcastUpdated(), reason = " + reason 184 + ", broadcastId = " + broadcastId); 185 } 186 mMainThreadHandler.post(() -> handleLeBroadcastUpdated()); 187 } 188 189 @Override 190 public void onBroadcastUpdateFailed(int reason, int broadcastId) { 191 if (DEBUG) { 192 Log.d(TAG, "onBroadcastUpdateFailed(), reason = " + reason 193 + ", broadcastId = " + broadcastId); 194 } 195 mMainThreadHandler.post(() -> handleLeBroadcastUpdateFailed()); 196 } 197 198 @Override 199 public void onPlaybackStarted(int reason, int broadcastId) { 200 } 201 202 @Override 203 public void onPlaybackStopped(int reason, int broadcastId) { 204 } 205 }; 206 207 private class LayoutManagerWrapper extends LinearLayoutManager { LayoutManagerWrapper(Context context)208 LayoutManagerWrapper(Context context) { 209 super(context); 210 } 211 212 @Override onLayoutCompleted(RecyclerView.State state)213 public void onLayoutCompleted(RecyclerView.State state) { 214 super.onLayoutCompleted(state); 215 mMediaOutputController.setRefreshing(false); 216 mMediaOutputController.refreshDataSetIfNeeded(); 217 } 218 } 219 MediaOutputBaseDialog( Context context, BroadcastSender broadcastSender, MediaOutputController mediaOutputController, boolean includePlaybackAndAppMetadata)220 public MediaOutputBaseDialog( 221 Context context, 222 BroadcastSender broadcastSender, 223 MediaOutputController mediaOutputController, 224 boolean includePlaybackAndAppMetadata) { 225 super(context, R.style.Theme_SystemUI_Dialog_Media); 226 227 // Save the context that is wrapped with our theme. 228 mContext = getContext(); 229 mBroadcastSender = broadcastSender; 230 mMediaOutputController = mediaOutputController; 231 mLayoutManager = new LayoutManagerWrapper(mContext); 232 mListMaxHeight = context.getResources().getDimensionPixelSize( 233 R.dimen.media_output_dialog_list_max_height); 234 mItemHeight = context.getResources().getDimensionPixelSize( 235 R.dimen.media_output_dialog_list_item_height); 236 mListPaddingTop = mContext.getResources().getDimensionPixelSize( 237 R.dimen.media_output_dialog_list_padding_top); 238 mExecutor = Executors.newSingleThreadExecutor(); 239 mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata; 240 } 241 242 @Override onCreate(Bundle savedInstanceState)243 public void onCreate(Bundle savedInstanceState) { 244 super.onCreate(savedInstanceState); 245 246 mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null); 247 final Window window = getWindow(); 248 final WindowManager.LayoutParams lp = window.getAttributes(); 249 lp.gravity = Gravity.CENTER; 250 // Config insets to make sure the layout is above the navigation bar 251 lp.setFitInsetsTypes(statusBars() | navigationBars()); 252 lp.setFitInsetsSides(WindowInsets.Side.all()); 253 lp.setFitInsetsIgnoringVisibility(true); 254 window.setAttributes(lp); 255 window.setContentView(mDialogView); 256 window.setTitle(mContext.getString(R.string.media_output_dialog_accessibility_title)); 257 window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 258 259 mHeaderTitle = mDialogView.requireViewById(R.id.header_title); 260 mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); 261 mHeaderIcon = mDialogView.requireViewById(R.id.header_icon); 262 mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result); 263 mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section); 264 mDeviceListLayout = mDialogView.requireViewById(R.id.device_list); 265 mDoneButton = mDialogView.requireViewById(R.id.done); 266 mStopButton = mDialogView.requireViewById(R.id.stop); 267 mAppButton = mDialogView.requireViewById(R.id.launch_app_button); 268 mAppResourceIcon = mDialogView.requireViewById(R.id.app_source_icon); 269 mCastAppLayout = mDialogView.requireViewById(R.id.cast_app_section); 270 mBroadcastIcon = mDialogView.requireViewById(R.id.broadcast_icon); 271 272 mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener( 273 mDeviceListLayoutListener); 274 // Init device list 275 mLayoutManager.setAutoMeasureEnabled(true); 276 mDevicesRecyclerView.setLayoutManager(mLayoutManager); 277 mDevicesRecyclerView.setAdapter(mAdapter); 278 mDevicesRecyclerView.setHasFixedSize(false); 279 // Init bottom buttons 280 mDoneButton.setOnClickListener(v -> dismiss()); 281 mStopButton.setOnClickListener(v -> onStopButtonClick()); 282 mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication); 283 mMediaMetadataSectionLayout.setOnClickListener( 284 mMediaOutputController::tryToLaunchMediaApplication); 285 286 mDismissing = false; 287 } 288 289 @Override dismiss()290 public void dismiss() { 291 // TODO(287191450): remove this once expensive binder calls are removed from refresh(). 292 // Due to these binder calls on the UI thread, calling refresh() during dismissal causes 293 // significant frame drops for the dismissal animation. Since the dialog is going away 294 // anyway, we use this state to turn refresh() into a no-op. 295 mDismissing = true; 296 super.dismiss(); 297 } 298 299 @Override start()300 public void start() { 301 mMediaOutputController.start(this); 302 if (isBroadcastSupported() && !mIsLeBroadcastCallbackRegistered) { 303 mMediaOutputController.registerLeBroadcastServiceCallback(mExecutor, 304 mBroadcastCallback); 305 mIsLeBroadcastCallbackRegistered = true; 306 } 307 } 308 309 @Override stop()310 public void stop() { 311 // unregister broadcast callback should only depend on profile and registered flag 312 // rather than remote device or broadcast state 313 // otherwise it might have risks of leaking registered callback handle 314 if (mMediaOutputController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { 315 mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback); 316 mIsLeBroadcastCallbackRegistered = false; 317 } 318 mMediaOutputController.stop(); 319 } 320 321 @VisibleForTesting refresh()322 void refresh() { 323 refresh(false); 324 } 325 refresh(boolean deviceSetChanged)326 void refresh(boolean deviceSetChanged) { 327 // TODO(287191450): remove binder calls in this method from the UI thread. 328 // If the dialog is going away or is already refreshing, do nothing. 329 if (mDismissing || mMediaOutputController.isRefreshing()) { 330 return; 331 } 332 mMediaOutputController.setRefreshing(true); 333 // Update header icon 334 final int iconRes = getHeaderIconRes(); 335 final IconCompat headerIcon = getHeaderIcon(); 336 final IconCompat appSourceIcon = getAppSourceIcon(); 337 boolean colorSetUpdated = false; 338 mCastAppLayout.setVisibility( 339 mMediaOutputController.shouldShowLaunchSection() 340 ? View.VISIBLE : View.GONE); 341 if (iconRes != 0) { 342 mHeaderIcon.setVisibility(View.VISIBLE); 343 mHeaderIcon.setImageResource(iconRes); 344 } else if (headerIcon != null) { 345 Icon icon = headerIcon.toIcon(mContext); 346 if (icon.getType() != Icon.TYPE_BITMAP && icon.getType() != Icon.TYPE_ADAPTIVE_BITMAP) { 347 // icon doesn't support getBitmap, use default value for color scheme 348 updateButtonBackgroundColorFilter(); 349 updateDialogBackgroundColor(); 350 } else { 351 Configuration config = mContext.getResources().getConfiguration(); 352 int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; 353 boolean isDarkThemeOn = currentNightMode == Configuration.UI_MODE_NIGHT_YES; 354 WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap()); 355 colorSetUpdated = !wallpaperColors.equals(mWallpaperColors); 356 if (colorSetUpdated) { 357 mAdapter.updateColorScheme(wallpaperColors, isDarkThemeOn); 358 updateButtonBackgroundColorFilter(); 359 updateDialogBackgroundColor(); 360 } 361 } 362 mHeaderIcon.setVisibility(View.VISIBLE); 363 mHeaderIcon.setImageIcon(icon); 364 } else { 365 updateButtonBackgroundColorFilter(); 366 updateDialogBackgroundColor(); 367 mHeaderIcon.setVisibility(View.GONE); 368 } 369 370 if (!mIncludePlaybackAndAppMetadata) { 371 mAppResourceIcon.setVisibility(View.GONE); 372 } else if (appSourceIcon != null) { 373 Icon appIcon = appSourceIcon.toIcon(mContext); 374 mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent()); 375 mAppResourceIcon.setImageIcon(appIcon); 376 } else { 377 Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage(); 378 if (appIconDrawable != null) { 379 mAppResourceIcon.setImageDrawable(appIconDrawable); 380 } else { 381 mAppResourceIcon.setVisibility(View.GONE); 382 } 383 } 384 if (mHeaderIcon.getVisibility() == View.VISIBLE) { 385 final int size = getHeaderIconSize(); 386 final int padding = mContext.getResources().getDimensionPixelSize( 387 R.dimen.media_output_dialog_header_icon_padding); 388 mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size)); 389 } 390 mAppButton.setText(mMediaOutputController.getAppSourceName()); 391 392 if (!mIncludePlaybackAndAppMetadata) { 393 mHeaderTitle.setVisibility(View.GONE); 394 mHeaderSubtitle.setVisibility(View.GONE); 395 } else { 396 // Update title and subtitle 397 mHeaderTitle.setText(getHeaderText()); 398 final CharSequence subTitle = getHeaderSubtitle(); 399 if (TextUtils.isEmpty(subTitle)) { 400 mHeaderSubtitle.setVisibility(View.GONE); 401 mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); 402 } else { 403 mHeaderSubtitle.setVisibility(View.VISIBLE); 404 mHeaderSubtitle.setText(subTitle); 405 mHeaderTitle.setGravity(Gravity.NO_GRAVITY); 406 } 407 } 408 409 // Show when remote media session is available or 410 // when the device supports BT LE audio + media is playing 411 mStopButton.setVisibility(getStopButtonVisibility()); 412 mStopButton.setEnabled(true); 413 mStopButton.setText(getStopButtonText()); 414 mStopButton.setOnClickListener(v -> onStopButtonClick()); 415 416 mBroadcastIcon.setVisibility(getBroadcastIconVisibility()); 417 mBroadcastIcon.setOnClickListener(v -> onBroadcastIconClick()); 418 if (!mAdapter.isDragging()) { 419 int currentActivePosition = mAdapter.getCurrentActivePosition(); 420 if (!colorSetUpdated && !deviceSetChanged && currentActivePosition >= 0 421 && currentActivePosition < mAdapter.getItemCount()) { 422 mAdapter.notifyItemChanged(currentActivePosition); 423 } else { 424 mAdapter.updateItems(); 425 } 426 } else { 427 mMediaOutputController.setRefreshing(false); 428 mMediaOutputController.refreshDataSetIfNeeded(); 429 } 430 } 431 updateButtonBackgroundColorFilter()432 private void updateButtonBackgroundColorFilter() { 433 ColorFilter buttonColorFilter = new PorterDuffColorFilter( 434 mMediaOutputController.getColorButtonBackground(), 435 PorterDuff.Mode.SRC_IN); 436 mDoneButton.getBackground().setColorFilter(buttonColorFilter); 437 mStopButton.getBackground().setColorFilter(buttonColorFilter); 438 mDoneButton.setTextColor(mMediaOutputController.getColorPositiveButtonText()); 439 } 440 updateDialogBackgroundColor()441 private void updateDialogBackgroundColor() { 442 getDialogView().getBackground().setTint(mMediaOutputController.getColorDialogBackground()); 443 mDeviceListLayout.setBackgroundColor(mMediaOutputController.getColorDialogBackground()); 444 } 445 resizeDrawable(Drawable drawable, int size)446 private Drawable resizeDrawable(Drawable drawable, int size) { 447 if (drawable == null) { 448 return null; 449 } 450 int width = drawable.getIntrinsicWidth(); 451 int height = drawable.getIntrinsicHeight(); 452 Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 453 : Bitmap.Config.RGB_565; 454 Bitmap bitmap = Bitmap.createBitmap(width, height, config); 455 Canvas canvas = new Canvas(bitmap); 456 drawable.setBounds(0, 0, width, height); 457 drawable.draw(canvas); 458 return new BitmapDrawable(mContext.getResources(), 459 Bitmap.createScaledBitmap(bitmap, size, size, false)); 460 } 461 handleLeBroadcastStarted()462 public void handleLeBroadcastStarted() { 463 // Waiting for the onBroadcastMetadataChanged. The UI launchs the broadcast dialog when 464 // the metadata is ready. 465 mShouldLaunchLeBroadcastDialog = true; 466 } 467 handleLeBroadcastStartFailed()468 public void handleLeBroadcastStartFailed() { 469 mStopButton.setText(R.string.media_output_broadcast_start_failed); 470 mStopButton.setEnabled(false); 471 refresh(); 472 } 473 handleLeBroadcastMetadataChanged()474 public void handleLeBroadcastMetadataChanged() { 475 if (mShouldLaunchLeBroadcastDialog) { 476 startLeBroadcastDialog(); 477 mShouldLaunchLeBroadcastDialog = false; 478 } 479 refresh(); 480 } 481 handleLeBroadcastStopped()482 public void handleLeBroadcastStopped() { 483 mShouldLaunchLeBroadcastDialog = false; 484 refresh(); 485 } 486 handleLeBroadcastStopFailed()487 public void handleLeBroadcastStopFailed() { 488 refresh(); 489 } 490 handleLeBroadcastUpdated()491 public void handleLeBroadcastUpdated() { 492 refresh(); 493 } 494 handleLeBroadcastUpdateFailed()495 public void handleLeBroadcastUpdateFailed() { 496 refresh(); 497 } 498 startLeBroadcast()499 protected void startLeBroadcast() { 500 mStopButton.setText(R.string.media_output_broadcast_starting); 501 mStopButton.setEnabled(false); 502 if (!mMediaOutputController.startBluetoothLeBroadcast()) { 503 // If the system can't execute "broadcast start", then UI shows the error. 504 handleLeBroadcastStartFailed(); 505 } 506 } 507 startLeBroadcastDialogForFirstTime()508 protected boolean startLeBroadcastDialogForFirstTime(){ 509 SharedPreferences sharedPref = mContext.getSharedPreferences(PREF_NAME, 510 Context.MODE_PRIVATE); 511 if (sharedPref != null 512 && sharedPref.getBoolean(PREF_IS_LE_BROADCAST_FIRST_LAUNCH, true)) { 513 Log.d(TAG, "PREF_IS_LE_BROADCAST_FIRST_LAUNCH: true"); 514 515 mMediaOutputController.launchLeBroadcastNotifyDialog(mDialogView, 516 mBroadcastSender, 517 MediaOutputController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH, 518 (d, w) -> { 519 startLeBroadcast(); 520 }); 521 SharedPreferences.Editor editor = sharedPref.edit(); 522 editor.putBoolean(PREF_IS_LE_BROADCAST_FIRST_LAUNCH, false); 523 editor.apply(); 524 return true; 525 } 526 return false; 527 } 528 startLeBroadcastDialog()529 protected void startLeBroadcastDialog() { 530 mMediaOutputController.launchMediaOutputBroadcastDialog(mDialogView, 531 mBroadcastSender); 532 refresh(); 533 } 534 stopLeBroadcast()535 protected void stopLeBroadcast() { 536 mStopButton.setEnabled(false); 537 if (!mMediaOutputController.stopBluetoothLeBroadcast()) { 538 // If the system can't execute "broadcast stop", then UI does refresh. 539 mMainThreadHandler.post(() -> refresh()); 540 } 541 } 542 getAppSourceIcon()543 abstract IconCompat getAppSourceIcon(); 544 getHeaderIconRes()545 abstract int getHeaderIconRes(); 546 getHeaderIcon()547 abstract IconCompat getHeaderIcon(); 548 getHeaderIconSize()549 abstract int getHeaderIconSize(); 550 getHeaderText()551 abstract CharSequence getHeaderText(); 552 getHeaderSubtitle()553 abstract CharSequence getHeaderSubtitle(); 554 getStopButtonVisibility()555 abstract int getStopButtonVisibility(); 556 getStopButtonText()557 public CharSequence getStopButtonText() { 558 return mContext.getText(R.string.keyboard_key_media_stop); 559 } 560 onStopButtonClick()561 public void onStopButtonClick() { 562 mMediaOutputController.releaseSession(); 563 dismiss(); 564 } 565 getBroadcastIconVisibility()566 public int getBroadcastIconVisibility() { 567 return View.GONE; 568 } 569 onBroadcastIconClick()570 public void onBroadcastIconClick() { 571 // Do nothing. 572 } 573 isBroadcastSupported()574 public boolean isBroadcastSupported() { 575 return false; 576 } 577 578 @Override onMediaChanged()579 public void onMediaChanged() { 580 mMainThreadHandler.post(() -> refresh()); 581 } 582 583 @Override onMediaStoppedOrPaused()584 public void onMediaStoppedOrPaused() { 585 if (isShowing()) { 586 dismiss(); 587 } 588 } 589 590 @Override onRouteChanged()591 public void onRouteChanged() { 592 mMainThreadHandler.post(() -> refresh()); 593 } 594 595 @Override onDeviceListChanged()596 public void onDeviceListChanged() { 597 mMainThreadHandler.post(() -> refresh(true)); 598 } 599 600 @Override dismissDialog()601 public void dismissDialog() { 602 mBroadcastSender.closeSystemDialogs(); 603 } 604 onHeaderIconClick()605 void onHeaderIconClick() { 606 } 607 getDialogView()608 View getDialogView() { 609 return mDialogView; 610 } 611 } 612