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