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