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