1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media.dialog;
18 
19 import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
20 
21 import android.app.AlertDialog;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothLeBroadcastAssistant;
24 import android.bluetooth.BluetoothLeBroadcastMetadata;
25 import android.bluetooth.BluetoothLeBroadcastReceiveState;
26 import android.content.Context;
27 import android.graphics.Bitmap;
28 import android.os.Bundle;
29 import android.text.Editable;
30 import android.text.TextWatcher;
31 import android.text.method.HideReturnsTransformationMethod;
32 import android.text.method.PasswordTransformationMethod;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewStub;
37 import android.view.WindowManager;
38 import android.widget.Button;
39 import android.widget.EditText;
40 import android.widget.ImageView;
41 import android.widget.TextView;
42 
43 import androidx.annotation.NonNull;
44 import androidx.core.graphics.drawable.IconCompat;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.settingslib.qrcode.QrCodeGenerator;
48 import com.android.systemui.broadcast.BroadcastSender;
49 import com.android.systemui.dagger.SysUISingleton;
50 import com.android.systemui.res.R;
51 import com.android.systemui.statusbar.phone.SystemUIDialog;
52 
53 import com.google.zxing.WriterException;
54 
55 /**
56  * Dialog for media output broadcast.
57  */
58 @SysUISingleton
59 public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
60     private static final String TAG = "MediaOutputBroadcastDialog";
61 
62     static final int METADATA_BROADCAST_NAME = 0;
63     static final int METADATA_BROADCAST_CODE = 1;
64 
65     private static final int MAX_BROADCAST_INFO_UPDATE = 3;
66     @VisibleForTesting
67     static final int BROADCAST_CODE_MAX_LENGTH = 16;
68     @VisibleForTesting
69     static final int BROADCAST_CODE_MIN_LENGTH = 4;
70     @VisibleForTesting
71     static final int BROADCAST_NAME_MAX_LENGTH = 254;
72 
73     private ViewStub mBroadcastInfoArea;
74     private ImageView mBroadcastQrCodeView;
75     private ImageView mBroadcastNotify;
76     private TextView mBroadcastName;
77     private ImageView mBroadcastNameEdit;
78     private TextView mBroadcastCode;
79     private ImageView mBroadcastCodeEye;
80     private Boolean mIsPasswordHide = true;
81     private ImageView mBroadcastCodeEdit;
82     @VisibleForTesting
83     AlertDialog mAlertDialog;
84     private TextView mBroadcastErrorMessage;
85     private int mRetryCount = 0;
86     private String mCurrentBroadcastName;
87     private String mCurrentBroadcastCode;
88     private boolean mIsStopbyUpdateBroadcastCode = false;
89     private boolean mIsLeBroadcastAssistantCallbackRegistered;
90 
91     private TextWatcher mBroadcastCodeTextWatcher = new TextWatcher() {
92         @Override
93         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
94             // Do nothing
95         }
96 
97         @Override
98         public void onTextChanged(CharSequence s, int start, int before, int count) {
99             // Do nothing
100         }
101 
102         @Override
103         public void afterTextChanged(Editable s) {
104             if (mAlertDialog == null || mBroadcastErrorMessage == null) {
105                 return;
106             }
107             boolean breakBroadcastCodeRuleTextLengthLessThanMin =
108                     s.length() > 0 && s.length() < BROADCAST_CODE_MIN_LENGTH;
109             boolean breakBroadcastCodeRuleTextLengthMoreThanMax =
110                     s.length() > BROADCAST_CODE_MAX_LENGTH;
111             boolean breakRule = breakBroadcastCodeRuleTextLengthLessThanMin
112                     || breakBroadcastCodeRuleTextLengthMoreThanMax;
113 
114             if (breakBroadcastCodeRuleTextLengthLessThanMin) {
115                 mBroadcastErrorMessage.setText(
116                         R.string.media_output_broadcast_code_hint_no_less_than_min);
117             } else if (breakBroadcastCodeRuleTextLengthMoreThanMax) {
118                 mBroadcastErrorMessage.setText(
119                         mContext.getResources().getString(
120                                 R.string.media_output_broadcast_edit_hint_no_more_than_max,
121                                 BROADCAST_CODE_MAX_LENGTH));
122             }
123 
124             mBroadcastErrorMessage.setVisibility(breakRule ? View.VISIBLE : View.INVISIBLE);
125             Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
126             if (positiveBtn != null) {
127                 positiveBtn.setEnabled(breakRule ? false : true);
128             }
129         }
130     };
131 
132     private TextWatcher mBroadcastNameTextWatcher = new TextWatcher() {
133         @Override
134         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
135             // Do nothing
136         }
137 
138         @Override
139         public void onTextChanged(CharSequence s, int start, int before, int count) {
140             // Do nothing
141         }
142 
143         @Override
144         public void afterTextChanged(Editable s) {
145             if (mAlertDialog == null || mBroadcastErrorMessage == null) {
146                 return;
147             }
148             boolean breakBroadcastNameRuleTextLengthMoreThanMax =
149                     s.length() > BROADCAST_NAME_MAX_LENGTH;
150             boolean breakRule = breakBroadcastNameRuleTextLengthMoreThanMax || (s.length() == 0);
151 
152             if (breakBroadcastNameRuleTextLengthMoreThanMax) {
153                 mBroadcastErrorMessage.setText(
154                         mContext.getResources().getString(
155                                 R.string.media_output_broadcast_edit_hint_no_more_than_max,
156                                 BROADCAST_NAME_MAX_LENGTH));
157             }
158             mBroadcastErrorMessage.setVisibility(
159                     breakBroadcastNameRuleTextLengthMoreThanMax ? View.VISIBLE : View.INVISIBLE);
160             Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
161             if (positiveBtn != null) {
162                 positiveBtn.setEnabled(breakRule ? false : true);
163             }
164         }
165     };
166 
167     private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
168             new BluetoothLeBroadcastAssistant.Callback() {
169                 @Override
170                 public void onSearchStarted(int reason) {
171                     Log.d(TAG, "Assistant-onSearchStarted: " + reason);
172                 }
173 
174                 @Override
175                 public void onSearchStartFailed(int reason) {
176                     Log.d(TAG, "Assistant-onSearchStartFailed: " + reason);
177                 }
178 
179                 @Override
180                 public void onSearchStopped(int reason) {
181                     Log.d(TAG, "Assistant-onSearchStopped: " + reason);
182                 }
183 
184                 @Override
185                 public void onSearchStopFailed(int reason) {
186                     Log.d(TAG, "Assistant-onSearchStopFailed: " + reason);
187                 }
188 
189                 @Override
190                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
191                     Log.d(TAG, "Assistant-onSourceFound:");
192                 }
193 
194                 @Override
195                 public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
196                     Log.d(TAG, "Assistant-onSourceAdded: Device: " + sink
197                             + ", sourceId: " + sourceId);
198                     mMainThreadHandler.post(() -> refreshUi());
199                 }
200 
201                 @Override
202                 public void onSourceAddFailed(@NonNull BluetoothDevice sink,
203                         @NonNull BluetoothLeBroadcastMetadata source, int reason) {
204                     Log.d(TAG, "Assistant-onSourceAddFailed: Device: " + sink);
205                 }
206 
207                 @Override
208                 public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId,
209                         int reason) {
210                     Log.d(TAG, "Assistant-onSourceModified:");
211                 }
212 
213                 @Override
214                 public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId,
215                         int reason) {
216                     Log.d(TAG, "Assistant-onSourceModifyFailed:");
217                 }
218 
219                 @Override
220                 public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId,
221                         int reason) {
222                     Log.d(TAG, "Assistant-onSourceRemoved:");
223                 }
224 
225                 @Override
226                 public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId,
227                         int reason) {
228                     Log.d(TAG, "Assistant-onSourceRemoveFailed:");
229                 }
230 
231                 @Override
232                 public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId,
233                         @NonNull BluetoothLeBroadcastReceiveState state) {
234                     Log.d(TAG, "Assistant-onReceiveStateChanged:");
235                 }
236             };
237 
MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender, MediaOutputController mediaOutputController)238     MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar,
239             BroadcastSender broadcastSender, MediaOutputController mediaOutputController) {
240         super(
241                 context,
242                 broadcastSender,
243                 mediaOutputController, /* includePlaybackAndAppMetadata */
244                 true);
245         mAdapter = new MediaOutputAdapter(mMediaOutputController);
246         // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
247         //  that extends MediaOutputBaseDialog
248         if (!aboveStatusbar) {
249             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
250         }
251     }
252 
253     @Override
onCreate(Bundle savedInstanceState)254     public void onCreate(Bundle savedInstanceState) {
255         super.onCreate(savedInstanceState);
256 
257         initBtQrCodeUI();
258     }
259 
260     @Override
start()261     public void start() {
262         super.start();
263         if (!mIsLeBroadcastAssistantCallbackRegistered) {
264             mIsLeBroadcastAssistantCallbackRegistered = true;
265             mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor,
266                     mBroadcastAssistantCallback);
267         }
268         /* Add local source broadcast to connected capable devices that may be possible receivers
269          * of stream.
270          */
271         startBroadcastWithConnectedDevices();
272     }
273 
274     @Override
stop()275     public void stop() {
276         super.stop();
277         if (mIsLeBroadcastAssistantCallbackRegistered) {
278             mIsLeBroadcastAssistantCallbackRegistered = false;
279             mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback(
280                     mBroadcastAssistantCallback);
281         }
282     }
283 
284     @Override
getHeaderIconRes()285     int getHeaderIconRes() {
286         return 0;
287     }
288 
289     @Override
getHeaderIcon()290     IconCompat getHeaderIcon() {
291         return mMediaOutputController.getHeaderIcon();
292     }
293 
294     @Override
getHeaderIconSize()295     int getHeaderIconSize() {
296         return mContext.getResources().getDimensionPixelSize(
297                 R.dimen.media_output_dialog_header_album_icon_size);
298     }
299 
300     @Override
getHeaderText()301     CharSequence getHeaderText() {
302         return mMediaOutputController.getHeaderTitle();
303     }
304 
305     @Override
getHeaderSubtitle()306     CharSequence getHeaderSubtitle() {
307         return mMediaOutputController.getHeaderSubTitle();
308     }
309 
310     @Override
getAppSourceIcon()311     IconCompat getAppSourceIcon() {
312         return mMediaOutputController.getNotificationSmallIcon();
313     }
314 
315     @Override
getStopButtonVisibility()316     int getStopButtonVisibility() {
317         return View.VISIBLE;
318     }
319 
320     @Override
onStopButtonClick()321     public void onStopButtonClick() {
322         mMediaOutputController.stopBluetoothLeBroadcast();
323         dismiss();
324     }
325 
getBroadcastMetadataInfo(int metadata)326     private String getBroadcastMetadataInfo(int metadata) {
327         switch (metadata) {
328             case METADATA_BROADCAST_NAME:
329                 return mMediaOutputController.getBroadcastName();
330             case METADATA_BROADCAST_CODE:
331                 return mMediaOutputController.getBroadcastCode();
332             default:
333                 return "";
334         }
335     }
336 
initBtQrCodeUI()337     private void initBtQrCodeUI() {
338         //add the view to xml
339         inflateBroadcastInfoArea();
340 
341         //init UI component
342         mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view);
343 
344         mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info);
345         mBroadcastNotify.setOnClickListener(v -> {
346             mMediaOutputController.launchLeBroadcastNotifyDialog(
347                     /* view= */ null,
348                     /* broadcastSender= */ null,
349                     MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON,
350                     /* onClickListener= */ null);
351         });
352         mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary);
353         mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit);
354         mBroadcastNameEdit.setOnClickListener(v -> {
355             launchBroadcastUpdatedDialog(false, mBroadcastName.getText().toString());
356         });
357         mBroadcastCode = getDialogView().requireViewById(R.id.broadcast_code_summary);
358         mBroadcastCode.setTransformationMethod(PasswordTransformationMethod.getInstance());
359         mBroadcastCodeEye = getDialogView().requireViewById(R.id.broadcast_code_eye);
360         mBroadcastCodeEye.setOnClickListener(v -> {
361             updateBroadcastCodeVisibility();
362         });
363         mBroadcastCodeEdit = getDialogView().requireViewById(R.id.broadcast_code_edit);
364         mBroadcastCodeEdit.setOnClickListener(v -> {
365             launchBroadcastUpdatedDialog(true, mBroadcastCode.getText().toString());
366         });
367 
368         refreshUi();
369     }
370 
refreshUi()371     private void refreshUi() {
372         setQrCodeView();
373 
374         mCurrentBroadcastName = getBroadcastMetadataInfo(METADATA_BROADCAST_NAME);
375         mCurrentBroadcastCode = getBroadcastMetadataInfo(METADATA_BROADCAST_CODE);
376         mBroadcastName.setText(mCurrentBroadcastName);
377         mBroadcastCode.setText(mCurrentBroadcastCode);
378         refresh(false);
379     }
380 
inflateBroadcastInfoArea()381     private void inflateBroadcastInfoArea() {
382         mBroadcastInfoArea = getDialogView().requireViewById(R.id.broadcast_qrcode);
383         mBroadcastInfoArea.inflate();
384     }
385 
setQrCodeView()386     private void setQrCodeView() {
387         //get the Metadata, and convert to BT QR code format.
388         String broadcastMetadata = getLocalBroadcastMetadataQrCodeString();
389         if (broadcastMetadata.isEmpty()) {
390             //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap
391             return;
392         }
393         try {
394             final int qrcodeSize = getContext().getResources().getDimensionPixelSize(
395                     R.dimen.media_output_qrcode_size);
396             final Bitmap bmp = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize);
397             mBroadcastQrCodeView.setImageBitmap(bmp);
398         } catch (WriterException e) {
399             //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap
400             Log.e(TAG, "Error generatirng QR code bitmap " + e);
401         }
402     }
403 
startBroadcastWithConnectedDevices()404     void startBroadcastWithConnectedDevices() {
405         //get the Metadata, and convert to BT QR code format.
406         BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
407         if (broadcastMetadata == null) {
408             Log.e(TAG, "Error: There is no broadcastMetadata.");
409             return;
410         }
411 
412         for (BluetoothDevice sink : mMediaOutputController.getConnectedBroadcastSinkDevices()) {
413             Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId()
414                     + ", the device: " + sink.getAnonymizedAddress());
415 
416             if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) {
417                 Log.d(TAG, "The sink device has the broadcast source now.");
418                 return;
419             }
420             if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink,
421                     broadcastMetadata, /*isGroupOp=*/ false)) {
422                 Log.e(TAG, "Error: Source add failed");
423             }
424         }
425     }
426 
updateBroadcastCodeVisibility()427     private void updateBroadcastCodeVisibility() {
428         mBroadcastCode.setTransformationMethod(
429                 mIsPasswordHide ? HideReturnsTransformationMethod.getInstance()
430                         : PasswordTransformationMethod.getInstance());
431         mIsPasswordHide = !mIsPasswordHide;
432     }
433 
launchBroadcastUpdatedDialog(boolean isBroadcastCode, String editString)434     private void launchBroadcastUpdatedDialog(boolean isBroadcastCode, String editString) {
435         final View layout = LayoutInflater.from(mContext).inflate(
436                 R.layout.media_output_broadcast_update_dialog, null);
437         final EditText editText = layout.requireViewById(R.id.broadcast_edit_text);
438         editText.setText(editString);
439         editText.addTextChangedListener(
440                 isBroadcastCode ? mBroadcastCodeTextWatcher : mBroadcastNameTextWatcher);
441         mBroadcastErrorMessage = layout.requireViewById(R.id.broadcast_error_message);
442         mAlertDialog = new Builder(mContext)
443                 .setTitle(isBroadcastCode ? R.string.media_output_broadcast_code
444                         : R.string.media_output_broadcast_name)
445                 .setView(layout)
446                 .setNegativeButton(android.R.string.cancel, null)
447                 .setPositiveButton(R.string.media_output_broadcast_dialog_save,
448                         (d, w) -> {
449                             updateBroadcastInfo(isBroadcastCode, editText.getText().toString());
450                         })
451                 .create();
452 
453         mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
454         SystemUIDialog.setShowForAllUsers(mAlertDialog, true);
455         SystemUIDialog.registerDismissListener(mAlertDialog);
456         mAlertDialog.show();
457     }
458 
getLocalBroadcastMetadataQrCodeString()459     private String getLocalBroadcastMetadataQrCodeString() {
460         return mMediaOutputController.getLocalBroadcastMetadataQrCodeString();
461     }
462 
getBroadcastMetadata()463     private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
464         return mMediaOutputController.getBroadcastMetadata();
465     }
466 
467     @VisibleForTesting
updateBroadcastInfo(boolean isBroadcastCode, String updatedString)468     void updateBroadcastInfo(boolean isBroadcastCode, String updatedString) {
469         Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
470         if (positiveBtn != null) {
471             positiveBtn.setEnabled(false);
472         }
473 
474         if (isBroadcastCode) {
475             /* If the user wants to update the Broadcast Code, the Broadcast session should be
476              * stopped then used the new Broadcast code to start the Broadcast.
477              */
478             mIsStopbyUpdateBroadcastCode = true;
479             mMediaOutputController.setBroadcastCode(updatedString);
480             if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
481                 handleLeBroadcastStopFailed();
482                 return;
483             }
484         } else {
485             /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast
486              * session. Only use the new Broadcast name to update the broadcast session.
487              */
488             mMediaOutputController.setBroadcastName(updatedString);
489             if (!mMediaOutputController.updateBluetoothLeBroadcast()) {
490                 handleLeBroadcastUpdateFailed();
491             }
492         }
493     }
494 
495     @Override
isBroadcastSupported()496     public boolean isBroadcastSupported() {
497         if (!legacyLeAudioSharing()) return false;
498         boolean isBluetoothLeDevice = false;
499         if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
500             isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice(
501                     mMediaOutputController.getCurrentConnectedMediaDevice());
502         }
503 
504         return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice;
505     }
506 
507     @Override
handleLeBroadcastStarted()508     public void handleLeBroadcastStarted() {
509         mRetryCount = 0;
510         if (mAlertDialog != null) {
511             mAlertDialog.dismiss();
512         }
513         refreshUi();
514     }
515 
516     @Override
handleLeBroadcastStartFailed()517     public void handleLeBroadcastStartFailed() {
518         mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
519         mRetryCount++;
520 
521         handleUpdateFailedUi();
522     }
523 
524     @Override
handleLeBroadcastMetadataChanged()525     public void handleLeBroadcastMetadataChanged() {
526         Log.d(TAG, "handleLeBroadcastMetadataChanged:");
527         refreshUi();
528     }
529 
530     @Override
handleLeBroadcastUpdated()531     public void handleLeBroadcastUpdated() {
532         mRetryCount = 0;
533         if (mAlertDialog != null) {
534             mAlertDialog.dismiss();
535         }
536         refreshUi();
537     }
538 
539     @Override
handleLeBroadcastUpdateFailed()540     public void handleLeBroadcastUpdateFailed() {
541         //Change the value in shared preferences back to it original value
542         mMediaOutputController.setBroadcastName(mCurrentBroadcastName);
543         mRetryCount++;
544 
545         handleUpdateFailedUi();
546     }
547 
548     @Override
handleLeBroadcastStopped()549     public void handleLeBroadcastStopped() {
550         if (mIsStopbyUpdateBroadcastCode) {
551             mIsStopbyUpdateBroadcastCode = false;
552             mRetryCount = 0;
553             if (!mMediaOutputController.startBluetoothLeBroadcast()) {
554                 handleLeBroadcastStartFailed();
555                 return;
556             }
557         } else {
558             dismiss();
559         }
560     }
561 
562     @Override
handleLeBroadcastStopFailed()563     public void handleLeBroadcastStopFailed() {
564         //Change the value in shared preferences back to it original value
565         mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
566         mRetryCount++;
567 
568         handleUpdateFailedUi();
569     }
570 
handleUpdateFailedUi()571     private void handleUpdateFailedUi() {
572         if (mAlertDialog == null) {
573             Log.d(TAG, "handleUpdateFailedUi: mAlertDialog is null");
574             return;
575         }
576         int errorMessageStringId = -1;
577         boolean enablePositiveBtn = false;
578         if (mRetryCount < MAX_BROADCAST_INFO_UPDATE) {
579             enablePositiveBtn = true;
580             errorMessageStringId = R.string.media_output_broadcast_update_error;
581         } else {
582             mRetryCount = 0;
583             errorMessageStringId = R.string.media_output_broadcast_last_update_error;
584         }
585 
586         // update UI
587         final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
588         if (positiveBtn != null && enablePositiveBtn) {
589             positiveBtn.setEnabled(true);
590         }
591         if (mBroadcastErrorMessage != null) {
592             mBroadcastErrorMessage.setVisibility(View.VISIBLE);
593             mBroadcastErrorMessage.setText(errorMessageStringId);
594         }
595     }
596 
597     @VisibleForTesting
getRetryCount()598     int getRetryCount() {
599         return mRetryCount;
600     }
601 }
602