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