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.content.ContentResolver; 23 import android.content.Context; 24 import android.content.SharedPreferences; 25 import android.database.ContentObserver; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.provider.Settings; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.lifecycle.DefaultLifecycleObserver; 34 import androidx.lifecycle.LifecycleOwner; 35 import androidx.preference.PreferenceScreen; 36 37 import com.android.settings.R; 38 import com.android.settings.bluetooth.Utils; 39 import com.android.settings.core.BasePreferenceController; 40 import com.android.settings.overlay.FeatureFactory; 41 import com.android.settings.widget.ValidatedEditTextPreference; 42 import com.android.settingslib.bluetooth.BluetoothUtils; 43 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; 44 import com.android.settingslib.bluetooth.LocalBluetoothManager; 45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 46 import com.android.settingslib.utils.ThreadUtils; 47 48 import java.nio.charset.StandardCharsets; 49 50 public class AudioSharingPasswordPreferenceController extends BasePreferenceController 51 implements ValidatedEditTextPreference.Validator, 52 AudioSharingPasswordPreference.OnDialogEventListener, 53 DefaultLifecycleObserver { 54 private static final String TAG = "AudioSharingPasswordPreferenceController"; 55 private static final String PREF_KEY = "audio_sharing_stream_password"; 56 private static final String SHARED_PREF_NAME = "audio_sharing_settings"; 57 private static final String SHARED_PREF_KEY = "default_password"; 58 @Nullable private final ContentResolver mContentResolver; 59 @Nullable private final SharedPreferences mSharedPref; 60 @Nullable private final LocalBluetoothManager mBtManager; 61 @Nullable private final LocalBluetoothLeBroadcast mBroadcast; 62 @Nullable private AudioSharingPasswordPreference mPreference; 63 private final ContentObserver mSettingsObserver; 64 private final SharedPreferences.OnSharedPreferenceChangeListener mSharedPrefChangeListener; 65 private final AudioSharingPasswordValidator mAudioSharingPasswordValidator; 66 private final MetricsFeatureProvider mMetricsFeatureProvider; 67 AudioSharingPasswordPreferenceController(Context context, String preferenceKey)68 public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) { 69 super(context, preferenceKey); 70 mBtManager = Utils.getLocalBluetoothManager(context); 71 mBroadcast = 72 mBtManager != null 73 ? mBtManager.getProfileManager().getLeAudioBroadcastProfile() 74 : null; 75 mAudioSharingPasswordValidator = new AudioSharingPasswordValidator(); 76 mContentResolver = context.getContentResolver(); 77 mSettingsObserver = new PasswordSettingsObserver(); 78 mSharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); 79 mSharedPrefChangeListener = new PasswordSharedPrefChangeListener(); 80 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 81 } 82 83 @Override onStart(@onNull LifecycleOwner owner)84 public void onStart(@NonNull LifecycleOwner owner) { 85 if (!isAvailable()) { 86 Log.d(TAG, "Feature is not available."); 87 return; 88 } 89 if (mContentResolver != null) { 90 mContentResolver.registerContentObserver( 91 Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), 92 false, 93 mSettingsObserver); 94 } 95 if (mSharedPref != null) { 96 mSharedPref.registerOnSharedPreferenceChangeListener(mSharedPrefChangeListener); 97 } 98 } 99 100 @Override onStop(@onNull LifecycleOwner owner)101 public void onStop(@NonNull LifecycleOwner owner) { 102 if (!isAvailable()) { 103 Log.d(TAG, "Feature is not available."); 104 return; 105 } 106 if (mContentResolver != null) { 107 mContentResolver.unregisterContentObserver(mSettingsObserver); 108 } 109 if (mSharedPref != null) { 110 mSharedPref.unregisterOnSharedPreferenceChangeListener(mSharedPrefChangeListener); 111 } 112 } 113 114 @Override getAvailabilityStatus()115 public int getAvailabilityStatus() { 116 return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE 117 : UNSUPPORTED_ON_DEVICE; 118 } 119 120 @Override displayPreference(PreferenceScreen screen)121 public void displayPreference(PreferenceScreen screen) { 122 super.displayPreference(screen); 123 mPreference = screen.findPreference(getPreferenceKey()); 124 if (mPreference != null) { 125 mPreference.setValidator(this); 126 mPreference.setIsPassword(true); 127 mPreference.setDialogLayoutResource(R.layout.audio_sharing_password_dialog); 128 mPreference.setOnDialogEventListener(this); 129 updatePreference(); 130 } 131 } 132 133 @Override getPreferenceKey()134 public String getPreferenceKey() { 135 return PREF_KEY; 136 } 137 138 @Override isTextValid(String value)139 public boolean isTextValid(String value) { 140 boolean isValid = mAudioSharingPasswordValidator.isTextValid(value); 141 if (mPreference != null) { 142 mPreference.showEditTextFormatAlert(!isValid); 143 } 144 return isValid; 145 } 146 147 @Override onBindDialogView()148 public void onBindDialogView() { 149 if (mPreference == null || mBroadcast == null) { 150 return; 151 } 152 mPreference.setEditable(!isBroadcasting(mBtManager)); 153 var password = mBroadcast.getBroadcastCode(); 154 mPreference.setChecked(isPublicBroadcast(password)); 155 } 156 157 @Override onPreferenceDataChanged(@onNull String password, boolean isPublicBroadcast)158 public void onPreferenceDataChanged(@NonNull String password, boolean isPublicBroadcast) { 159 var unused = 160 ThreadUtils.postOnBackgroundThread( 161 () -> { 162 if (mBroadcast == null || isBroadcasting(mBtManager)) { 163 Log.w( 164 TAG, 165 "onPreferenceDataChanged() changing password when" 166 + " broadcasting or null!"); 167 return; 168 } 169 boolean isCurrentPublicBroadcast = 170 isPublicBroadcast(mBroadcast.getBroadcastCode()); 171 String currentDefaultPassword = getDefaultPassword(mContext); 172 if (password.equals(currentDefaultPassword) 173 && isCurrentPublicBroadcast == isPublicBroadcast) { 174 Log.d(TAG, "onPreferenceDataChanged() nothing changed"); 175 return; 176 } 177 persistDefaultPassword(mContext, password); 178 mBroadcast.setBroadcastCode( 179 isPublicBroadcast ? new byte[0] : password.getBytes()); 180 mMetricsFeatureProvider.action( 181 mContext, 182 SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, 183 isPublicBroadcast ? 1 : 0); 184 }); 185 } 186 updatePreference()187 private void updatePreference() { 188 if (mBroadcast == null || mPreference == null) { 189 return; 190 } 191 var unused = 192 ThreadUtils.postOnBackgroundThread( 193 () -> { 194 byte[] password = mBroadcast.getBroadcastCode(); 195 boolean noPassword = isPublicBroadcast(password); 196 String passwordToDisplay = 197 noPassword 198 ? getDefaultPassword(mContext) 199 : new String(password, StandardCharsets.UTF_8); 200 String passwordSummary = 201 noPassword 202 ? mContext.getString( 203 R.string.audio_streams_no_password_summary) 204 : "********"; 205 206 AudioSharingUtils.postOnMainThread( 207 mContext, 208 () -> { 209 // Check nullability to pass NullAway check 210 if (mPreference != null) { 211 mPreference.setText(passwordToDisplay); 212 mPreference.setSummary(passwordSummary); 213 } 214 }); 215 }); 216 } 217 218 private class PasswordSettingsObserver extends ContentObserver { PasswordSettingsObserver()219 PasswordSettingsObserver() { 220 super(new Handler(Looper.getMainLooper())); 221 } 222 223 @Override onChange(boolean selfChange)224 public void onChange(boolean selfChange) { 225 Log.d(TAG, "onChange, broadcast password has been changed"); 226 updatePreference(); 227 } 228 } 229 230 private class PasswordSharedPrefChangeListener 231 implements SharedPreferences.OnSharedPreferenceChangeListener { 232 @Override onSharedPreferenceChanged( SharedPreferences sharedPreferences, @Nullable String key)233 public void onSharedPreferenceChanged( 234 SharedPreferences sharedPreferences, @Nullable String key) { 235 if (!SHARED_PREF_KEY.equals(key)) { 236 return; 237 } 238 Log.d(TAG, "onSharedPreferenceChanged, default password has been changed"); 239 updatePreference(); 240 } 241 } 242 persistDefaultPassword(Context context, String defaultPassword)243 private void persistDefaultPassword(Context context, String defaultPassword) { 244 if (getDefaultPassword(context).equals(defaultPassword)) { 245 return; 246 } 247 if (mSharedPref == null) { 248 Log.w(TAG, "persistDefaultPassword(): sharedPref is empty!"); 249 return; 250 } 251 252 SharedPreferences.Editor editor = mSharedPref.edit(); 253 editor.putString(SHARED_PREF_KEY, defaultPassword); 254 editor.apply(); 255 } 256 getDefaultPassword(Context context)257 private String getDefaultPassword(Context context) { 258 if (mSharedPref == null) { 259 Log.w(TAG, "getDefaultPassword(): sharedPref is empty!"); 260 return ""; 261 } 262 263 String value = mSharedPref.getString(SHARED_PREF_KEY, ""); 264 if (value != null && value.isEmpty()) { 265 Log.w(TAG, "getDefaultPassword(): default password is empty!"); 266 } 267 return value; 268 } 269 isPublicBroadcast(@ullable byte[] password)270 private static boolean isPublicBroadcast(@Nullable byte[] password) { 271 return password == null || password.length == 0; 272 } 273 } 274