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