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