1 /*
2  * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
18 
19 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
20 
21 import android.app.settings.SettingsEnums;
22 import android.bluetooth.BluetoothLeBroadcast;
23 import android.bluetooth.BluetoothLeBroadcastMetadata;
24 import android.content.Context;
25 import android.util.Log;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.lifecycle.DefaultLifecycleObserver;
30 import androidx.lifecycle.LifecycleOwner;
31 import androidx.preference.Preference;
32 import androidx.preference.PreferenceScreen;
33 
34 import com.android.settings.bluetooth.Utils;
35 import com.android.settings.core.BasePreferenceController;
36 import com.android.settings.overlay.FeatureFactory;
37 import com.android.settings.widget.ValidatedEditTextPreference;
38 import com.android.settingslib.bluetooth.BluetoothUtils;
39 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
40 import com.android.settingslib.bluetooth.LocalBluetoothManager;
41 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
42 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
43 import com.android.settingslib.utils.ThreadUtils;
44 
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.Executors;
47 import java.util.concurrent.atomic.AtomicBoolean;
48 
49 public class AudioSharingNamePreferenceController extends BasePreferenceController
50         implements ValidatedEditTextPreference.Validator,
51                 Preference.OnPreferenceChangeListener,
52                 DefaultLifecycleObserver,
53                 LocalBluetoothProfileManager.ServiceListener {
54 
55     private static final String TAG = "AudioSharingNamePreferenceController";
56     private static final boolean DEBUG = BluetoothUtils.D;
57     private static final String PREF_KEY = "audio_sharing_stream_name";
58 
59     private final BluetoothLeBroadcast.Callback mBroadcastCallback =
60             new BluetoothLeBroadcast.Callback() {
61                 @Override
62                 public void onBroadcastMetadataChanged(
63                         int broadcastId, BluetoothLeBroadcastMetadata metadata) {
64                     if (DEBUG) {
65                         Log.d(
66                                 TAG,
67                                 "onBroadcastMetadataChanged() broadcastId : "
68                                         + broadcastId
69                                         + " metadata: "
70                                         + metadata);
71                     }
72                     updateQrCodeIcon(true);
73                 }
74 
75                 @Override
76                 public void onBroadcastStartFailed(int reason) {}
77 
78                 @Override
79                 public void onBroadcastStarted(int reason, int broadcastId) {}
80 
81                 @Override
82                 public void onBroadcastStopFailed(int reason) {}
83 
84                 @Override
85                 public void onBroadcastStopped(int reason, int broadcastId) {
86                     if (DEBUG) {
87                         Log.d(
88                                 TAG,
89                                 "onBroadcastStopped() reason : "
90                                         + reason
91                                         + " broadcastId: "
92                                         + broadcastId);
93                     }
94                     updateQrCodeIcon(false);
95                 }
96 
97                 @Override
98                 public void onBroadcastUpdateFailed(int reason, int broadcastId) {
99                     Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
100                 }
101 
102                 @Override
103                 public void onBroadcastUpdated(int reason, int broadcastId) {
104                     if (DEBUG) {
105                         Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
106                     }
107                 }
108 
109                 @Override
110                 public void onPlaybackStarted(int reason, int broadcastId) {}
111 
112                 @Override
113                 public void onPlaybackStopped(int reason, int broadcastId) {}
114             };
115 
116     @Nullable private final LocalBluetoothManager mBtManager;
117     @Nullable private final LocalBluetoothProfileManager mProfileManager;
118     @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
119     @Nullable private AudioSharingNamePreference mPreference;
120     private final Executor mExecutor;
121     private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
122 
123     private final MetricsFeatureProvider mMetricsFeatureProvider;
124     private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
125 
AudioSharingNamePreferenceController(Context context, String preferenceKey)126     public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
127         super(context, preferenceKey);
128         mBtManager = Utils.getLocalBluetoothManager(context);
129         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
130         mBroadcast =
131                 (mProfileManager != null) ? mProfileManager.getLeAudioBroadcastProfile() : null;
132         mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
133         mExecutor = Executors.newSingleThreadExecutor();
134         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
135     }
136 
137     @Override
onStart(@onNull LifecycleOwner owner)138     public void onStart(@NonNull LifecycleOwner owner) {
139         if (!isAvailable()) {
140             Log.d(TAG, "Skip register callbacks, feature not support");
141             return;
142         }
143         if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
144             Log.d(TAG, "Skip register callbacks, profile not ready");
145             if (mProfileManager != null) {
146                 mProfileManager.addServiceListener(this);
147             }
148             return;
149         }
150         registerCallbacks();
151     }
152 
153     @Override
onStop(@onNull LifecycleOwner owner)154     public void onStop(@NonNull LifecycleOwner owner) {
155         if (!isAvailable()) {
156             Log.d(TAG, "Skip unregister callbacks, feature not support");
157             return;
158         }
159         if (mProfileManager != null) {
160             mProfileManager.removeServiceListener(this);
161         }
162         if (mBroadcast == null || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
163             Log.d(TAG, "Skip unregister callbacks, profile not ready");
164             return;
165         }
166         if (mCallbacksRegistered.get()) {
167             Log.d(TAG, "Unregister callbacks");
168             mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
169             mCallbacksRegistered.set(false);
170         }
171     }
172 
173     @Override
getAvailabilityStatus()174     public int getAvailabilityStatus() {
175         return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
176     }
177 
178     @Override
displayPreference(PreferenceScreen screen)179     public void displayPreference(PreferenceScreen screen) {
180         super.displayPreference(screen);
181         mPreference = screen.findPreference(getPreferenceKey());
182         if (mPreference != null) {
183             mPreference.setValidator(this);
184             updateBroadcastName();
185             updateQrCodeIcon(isBroadcasting(mBtManager));
186         }
187     }
188 
189     @Override
onServiceConnected()190     public void onServiceConnected() {
191         if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
192             registerCallbacks();
193             updateBroadcastName();
194             updateQrCodeIcon(isBroadcasting(mBtManager));
195             if (mProfileManager != null) {
196                 mProfileManager.removeServiceListener(this);
197             }
198         }
199     }
200 
201     @Override
onServiceDisconnected()202     public void onServiceDisconnected() {
203         // Do nothing
204     }
205 
206     @Override
getPreferenceKey()207     public String getPreferenceKey() {
208         return PREF_KEY;
209     }
210 
211     @Override
onPreferenceChange(Preference preference, Object newValue)212     public boolean onPreferenceChange(Preference preference, Object newValue) {
213         if (mPreference != null
214                 && mPreference.getSummary() != null
215                 && ((String) newValue).contentEquals(mPreference.getSummary())) {
216             return false;
217         }
218 
219         var unused =
220                 ThreadUtils.postOnBackgroundThread(
221                         () -> {
222                             if (mBroadcast != null) {
223                                 boolean isBroadcasting = isBroadcasting(mBtManager);
224                                 mBroadcast.setBroadcastName((String) newValue);
225                                 // We currently don't have a UI field for program info so we keep it
226                                 // consistent with broadcast name.
227                                 mBroadcast.setProgramInfo((String) newValue);
228                                 if (isBroadcasting) {
229                                     mBroadcast.updateBroadcast();
230                                 }
231                                 updateBroadcastName();
232                                 mMetricsFeatureProvider.action(
233                                         mContext,
234                                         SettingsEnums.ACTION_AUDIO_STREAM_NAME_UPDATED,
235                                         isBroadcasting ? 1 : 0);
236                             }
237                         });
238         return true;
239     }
240 
registerCallbacks()241     private void registerCallbacks() {
242         if (mBroadcast == null) {
243             Log.d(TAG, "Skip register callbacks, profile not ready");
244             return;
245         }
246         if (!mCallbacksRegistered.get()) {
247             Log.d(TAG, "Register callbacks");
248             mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
249             mCallbacksRegistered.set(true);
250         }
251     }
252 
updateBroadcastName()253     private void updateBroadcastName() {
254         if (mPreference != null) {
255             var unused =
256                     ThreadUtils.postOnBackgroundThread(
257                             () -> {
258                                 if (mBroadcast != null) {
259                                     String name = mBroadcast.getBroadcastName();
260                                     AudioSharingUtils.postOnMainThread(
261                                             mContext,
262                                             () -> {
263                                                 if (mPreference != null) {
264                                                     mPreference.setText(name);
265                                                     mPreference.setSummary(name);
266                                                 }
267                                             });
268                                 }
269                             });
270         }
271     }
272 
updateQrCodeIcon(boolean show)273     private void updateQrCodeIcon(boolean show) {
274         if (mPreference != null) {
275             AudioSharingUtils.postOnMainThread(
276                     mContext,
277                     () -> {
278                         if (mPreference != null) {
279                             mPreference.setShowQrCodeIcon(show);
280                         }
281                     });
282         }
283     }
284 
285     @Override
isTextValid(String value)286     public boolean isTextValid(String value) {
287         return mAudioSharingNameTextValidator.isTextValid(value);
288     }
289 }
290