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.bluetooth;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.NonNull;
21 import android.bluetooth.BluetoothLeBroadcast;
22 import android.bluetooth.BluetoothLeBroadcastMetadata;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.Window;
32 import android.widget.Button;
33 import android.widget.TextView;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.logging.UiEvent;
37 import com.android.internal.logging.UiEventLogger;
38 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
39 import com.android.settingslib.bluetooth.LocalBluetoothManager;
40 import com.android.settingslib.media.MediaOutputConstants;
41 import com.android.systemui.R;
42 import com.android.systemui.broadcast.BroadcastSender;
43 import com.android.systemui.media.controls.util.MediaDataUtils;
44 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
45 import com.android.systemui.statusbar.phone.SystemUIDialog;
46 
47 import java.util.concurrent.Executor;
48 import java.util.concurrent.Executors;
49 
50 /**
51  * Dialog for showing le audio broadcasting dialog.
52  */
53 public class BroadcastDialog extends SystemUIDialog {
54 
55     private static final String TAG = "BroadcastDialog";
56     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57     private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000;
58 
59     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
60 
61     private Context mContext;
62     private UiEventLogger mUiEventLogger;
63     @VisibleForTesting
64     protected View mDialogView;
65     private MediaOutputDialogFactory mMediaOutputDialogFactory;
66     private LocalBluetoothManager mLocalBluetoothManager;
67     private BroadcastSender mBroadcastSender;
68     private String mCurrentBroadcastApp;
69     private String mOutputPackageName;
70     private Executor mExecutor;
71     private boolean mShouldLaunchLeBroadcastDialog;
72     private Button mSwitchBroadcast;
73 
74     private final BluetoothLeBroadcast.Callback mBroadcastCallback =
75             new BluetoothLeBroadcast.Callback() {
76             @Override
77             public void onBroadcastStarted(int reason, int broadcastId) {
78                 if (DEBUG) {
79                     Log.d(TAG, "onBroadcastStarted(), reason = " + reason
80                             + ", broadcastId = " + broadcastId);
81                 }
82                 mMainThreadHandler.post(() -> handleLeBroadcastStarted());
83             }
84 
85             @Override
86             public void onBroadcastStartFailed(int reason) {
87                 if (DEBUG) {
88                     Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
89                 }
90                 mMainThreadHandler.postDelayed(() -> handleLeBroadcastStartFailed(),
91                         HANDLE_BROADCAST_FAILED_DELAY);
92             }
93 
94             @Override
95             public void onBroadcastMetadataChanged(int broadcastId,
96                     @NonNull BluetoothLeBroadcastMetadata metadata) {
97                 if (DEBUG) {
98                     Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId
99                             + ", metadata = " + metadata);
100                 }
101                 mMainThreadHandler.post(() -> handleLeBroadcastMetadataChanged());
102             }
103 
104             @Override
105             public void onBroadcastStopped(int reason, int broadcastId) {
106                 if (DEBUG) {
107                     Log.d(TAG, "onBroadcastStopped(), reason = " + reason
108                             + ", broadcastId = " + broadcastId);
109                 }
110                 mMainThreadHandler.post(() -> handleLeBroadcastStopped());
111             }
112 
113             @Override
114             public void onBroadcastStopFailed(int reason) {
115                 if (DEBUG) {
116                     Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
117                 }
118                 mMainThreadHandler.postDelayed(() -> handleLeBroadcastStopFailed(),
119                         HANDLE_BROADCAST_FAILED_DELAY);
120             }
121 
122             @Override
123             public void onBroadcastUpdated(int reason, int broadcastId) {
124             }
125 
126             @Override
127             public void onBroadcastUpdateFailed(int reason, int broadcastId) {
128             }
129 
130             @Override
131             public void onPlaybackStarted(int reason, int broadcastId) {
132             }
133 
134             @Override
135             public void onPlaybackStopped(int reason, int broadcastId) {
136             }
137         };
138 
BroadcastDialog(Context context, MediaOutputDialogFactory mediaOutputDialogFactory, LocalBluetoothManager localBluetoothManager, String currentBroadcastApp, String outputPkgName, UiEventLogger uiEventLogger, BroadcastSender broadcastSender)139     public BroadcastDialog(Context context, MediaOutputDialogFactory mediaOutputDialogFactory,
140             LocalBluetoothManager localBluetoothManager, String currentBroadcastApp,
141             String outputPkgName, UiEventLogger uiEventLogger, BroadcastSender broadcastSender) {
142         super(context);
143         if (DEBUG) {
144             Log.d(TAG, "Init BroadcastDialog");
145         }
146 
147         mContext = getContext();
148         mMediaOutputDialogFactory = mediaOutputDialogFactory;
149         mLocalBluetoothManager = localBluetoothManager;
150         mCurrentBroadcastApp = currentBroadcastApp;
151         mOutputPackageName = outputPkgName;
152         mUiEventLogger = uiEventLogger;
153         mExecutor = Executors.newSingleThreadExecutor();
154         mBroadcastSender = broadcastSender;
155     }
156 
157     @Override
start()158     public void start() {
159         registerBroadcastCallBack(mExecutor, mBroadcastCallback);
160     }
161 
162     @Override
onCreate(Bundle savedInstanceState)163     public void onCreate(Bundle savedInstanceState) {
164         super.onCreate(savedInstanceState);
165         if (DEBUG) {
166             Log.d(TAG, "onCreate");
167         }
168 
169         mUiEventLogger.log(BroadcastDialogEvent.BROADCAST_DIALOG_SHOW);
170         mDialogView = LayoutInflater.from(mContext).inflate(R.layout.broadcast_dialog, null);
171         final Window window = getWindow();
172         window.setContentView(mDialogView);
173 
174         TextView title = mDialogView.requireViewById(R.id.dialog_title);
175         TextView subTitle = mDialogView.requireViewById(R.id.dialog_subtitle);
176         title.setText(mContext.getString(
177                 R.string.bt_le_audio_broadcast_dialog_title, mCurrentBroadcastApp));
178         String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName,
179                 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name));
180         subTitle.setText(mContext.getString(
181                 R.string.bt_le_audio_broadcast_dialog_sub_title, switchBroadcastApp));
182 
183         mSwitchBroadcast = mDialogView.requireViewById(R.id.switch_broadcast);
184         Button changeOutput = mDialogView.requireViewById(R.id.change_output);
185         Button cancelBtn = mDialogView.requireViewById(R.id.cancel);
186         mSwitchBroadcast.setText(mContext.getString(
187                 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null);
188         mSwitchBroadcast.setOnClickListener((view) -> startSwitchBroadcast());
189         changeOutput.setOnClickListener((view) -> {
190             mMediaOutputDialogFactory.create(mOutputPackageName, true, null);
191             dismiss();
192         });
193         cancelBtn.setOnClickListener((view) -> {
194             if (DEBUG) {
195                 Log.d(TAG, "BroadcastDialog dismiss.");
196             }
197             dismiss();
198         });
199     }
200 
201     @Override
stop()202     public void stop() {
203         unregisterBroadcastCallBack(mBroadcastCallback);
204     }
205 
refreshSwitchBroadcastButton()206     void refreshSwitchBroadcastButton() {
207         String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName,
208                 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name));
209         mSwitchBroadcast.setText(mContext.getString(
210                 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null);
211         mSwitchBroadcast.setEnabled(true);
212     }
213 
startSwitchBroadcast()214     private void startSwitchBroadcast() {
215         if (DEBUG) {
216             Log.d(TAG, "startSwitchBroadcast");
217         }
218         mSwitchBroadcast.setText(R.string.media_output_broadcast_starting);
219         mSwitchBroadcast.setEnabled(false);
220         //Stop the current Broadcast
221         if (!stopBluetoothLeBroadcast()) {
222             handleLeBroadcastStopFailed();
223             return;
224         }
225     }
226 
registerBroadcastCallBack( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback)227     private void registerBroadcastCallBack(
228             @NonNull @CallbackExecutor Executor executor,
229             @NonNull BluetoothLeBroadcast.Callback callback) {
230         LocalBluetoothLeBroadcast broadcast =
231                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
232         if (broadcast == null) {
233             Log.d(TAG, "The broadcast profile is null");
234             return;
235         }
236         broadcast.registerServiceCallBack(executor, callback);
237     }
238 
unregisterBroadcastCallBack(@onNull BluetoothLeBroadcast.Callback callback)239     private void unregisterBroadcastCallBack(@NonNull BluetoothLeBroadcast.Callback callback) {
240         LocalBluetoothLeBroadcast broadcast =
241                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
242         if (broadcast == null) {
243             Log.d(TAG, "The broadcast profile is null");
244             return;
245         }
246         broadcast.unregisterServiceCallBack(callback);
247     }
248 
startBluetoothLeBroadcast()249     boolean startBluetoothLeBroadcast() {
250         LocalBluetoothLeBroadcast broadcast =
251                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
252         if (broadcast == null) {
253             Log.d(TAG, "The broadcast profile is null");
254             return false;
255         }
256         String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName,
257                 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name));
258         broadcast.startBroadcast(switchBroadcastApp, /*language*/ null);
259         return true;
260     }
261 
stopBluetoothLeBroadcast()262     boolean stopBluetoothLeBroadcast() {
263         LocalBluetoothLeBroadcast broadcast =
264                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
265         if (broadcast == null) {
266             Log.d(TAG, "The broadcast profile is null");
267             return false;
268         }
269         broadcast.stopLatestBroadcast();
270         return true;
271     }
272 
273     @Override
onWindowFocusChanged(boolean hasFocus)274     public void onWindowFocusChanged(boolean hasFocus) {
275         super.onWindowFocusChanged(hasFocus);
276         if (!hasFocus && isShowing()) {
277             dismiss();
278         }
279     }
280 
281     public enum BroadcastDialogEvent implements UiEventLogger.UiEventEnum {
282         @UiEvent(doc = "The Broadcast dialog became visible on the screen.")
283         BROADCAST_DIALOG_SHOW(1062);
284 
285         private final int mId;
286 
BroadcastDialogEvent(int id)287         BroadcastDialogEvent(int id) {
288             mId = id;
289         }
290 
291         @Override
getId()292         public int getId() {
293             return mId;
294         }
295     }
296 
handleLeBroadcastStarted()297     void handleLeBroadcastStarted() {
298         // Waiting for the onBroadcastMetadataChanged. The UI launchs the broadcast dialog when
299         // the metadata is ready.
300         mShouldLaunchLeBroadcastDialog = true;
301     }
302 
handleLeBroadcastStartFailed()303     private void handleLeBroadcastStartFailed() {
304         mSwitchBroadcast.setText(R.string.media_output_broadcast_start_failed);
305         mSwitchBroadcast.setEnabled(false);
306         refreshSwitchBroadcastButton();
307     }
308 
handleLeBroadcastMetadataChanged()309     void handleLeBroadcastMetadataChanged() {
310         if (mShouldLaunchLeBroadcastDialog) {
311             startLeBroadcastDialog();
312             mShouldLaunchLeBroadcastDialog = false;
313         }
314     }
315 
316     @VisibleForTesting
handleLeBroadcastStopped()317     void handleLeBroadcastStopped() {
318         mShouldLaunchLeBroadcastDialog = false;
319         if (!startBluetoothLeBroadcast()) {
320             handleLeBroadcastStartFailed();
321             return;
322         }
323     }
324 
handleLeBroadcastStopFailed()325     private void handleLeBroadcastStopFailed() {
326         mSwitchBroadcast.setText(R.string.media_output_broadcast_start_failed);
327         mSwitchBroadcast.setEnabled(false);
328         refreshSwitchBroadcastButton();
329     }
330 
startLeBroadcastDialog()331     private void startLeBroadcastDialog() {
332         mBroadcastSender.sendBroadcast(new Intent()
333                 .setPackage(mContext.getPackageName())
334                 .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG)
335                 .putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, mOutputPackageName));
336         dismiss();
337     }
338 }
339