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.server.audio; 18 19 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; 20 import static android.media.AudioSystem.DEVICE_NONE; 21 import static android.media.AudioSystem.isBluetoothDevice; 22 23 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.media.AudioDeviceAttributes; 28 import android.media.AudioDeviceInfo; 29 import android.media.AudioManager; 30 import android.media.Utils; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.Pair; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 37 import java.util.Objects; 38 39 /** 40 * Class representing all devices that were previously or are currently connected. Data is 41 * persisted in {@link android.provider.Settings.Secure} 42 */ 43 @VisibleForTesting(visibility = PACKAGE) 44 public final class AdiDeviceState { 45 private static final String TAG = "AS.AdiDeviceState"; 46 47 private static final String SETTING_FIELD_SEPARATOR = ","; 48 49 @AudioDeviceInfo.AudioDeviceType 50 private final int mDeviceType; 51 52 private final int mInternalDeviceType; 53 54 @NonNull 55 private final String mDeviceAddress; 56 57 /** Unique device id from internal device type and address. */ 58 private final Pair<Integer, String> mDeviceId; 59 60 @AudioManager.AudioDeviceCategory 61 private int mAudioDeviceCategory = AUDIO_DEVICE_CATEGORY_UNKNOWN; 62 63 private boolean mAutoBtCategorySet = false; 64 65 private boolean mSAEnabled; 66 private boolean mHasHeadTracker = false; 67 private boolean mHeadTrackerEnabled; 68 69 /** 70 * Constructor 71 * 72 * @param deviceType external audio device type 73 * @param internalDeviceType if not set pass {@link DEVICE_NONE}, in this case the 74 * default conversion of the external type will be used 75 * @param address must be non-null for wireless devices 76 * @throws NullPointerException if a null address is passed for a wireless device 77 */ AdiDeviceState(@udioDeviceInfo.AudioDeviceType int deviceType, int internalDeviceType, @Nullable String address)78 AdiDeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, 79 int internalDeviceType, 80 @Nullable String address) { 81 mDeviceType = deviceType; 82 if (internalDeviceType != DEVICE_NONE) { 83 mInternalDeviceType = internalDeviceType; 84 } else { 85 mInternalDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType); 86 87 } 88 mDeviceAddress = isBluetoothDevice(mInternalDeviceType) ? Objects.requireNonNull( 89 address) : ""; 90 91 mDeviceId = new Pair<>(mInternalDeviceType, mDeviceAddress); 92 } 93 getDeviceId()94 public synchronized Pair<Integer, String> getDeviceId() { 95 return mDeviceId; 96 } 97 98 @AudioDeviceInfo.AudioDeviceType getDeviceType()99 public synchronized int getDeviceType() { 100 return mDeviceType; 101 } 102 getInternalDeviceType()103 public synchronized int getInternalDeviceType() { 104 return mInternalDeviceType; 105 } 106 107 @NonNull getDeviceAddress()108 public synchronized String getDeviceAddress() { 109 return mDeviceAddress; 110 } 111 setSAEnabled(boolean sAEnabled)112 public synchronized void setSAEnabled(boolean sAEnabled) { 113 mSAEnabled = sAEnabled; 114 } 115 isSAEnabled()116 public synchronized boolean isSAEnabled() { 117 return mSAEnabled; 118 } 119 setHeadTrackerEnabled(boolean headTrackerEnabled)120 public synchronized void setHeadTrackerEnabled(boolean headTrackerEnabled) { 121 mHeadTrackerEnabled = headTrackerEnabled; 122 } 123 isHeadTrackerEnabled()124 public synchronized boolean isHeadTrackerEnabled() { 125 return mHeadTrackerEnabled; 126 } 127 setHasHeadTracker(boolean hasHeadTracker)128 public synchronized void setHasHeadTracker(boolean hasHeadTracker) { 129 mHasHeadTracker = hasHeadTracker; 130 } 131 132 hasHeadTracker()133 public synchronized boolean hasHeadTracker() { 134 return mHasHeadTracker; 135 } 136 137 @AudioDeviceInfo.AudioDeviceType getAudioDeviceCategory()138 public synchronized int getAudioDeviceCategory() { 139 return mAudioDeviceCategory; 140 } 141 setAudioDeviceCategory( @udioDeviceInfo.AudioDeviceType int audioDeviceCategory)142 public synchronized void setAudioDeviceCategory( 143 @AudioDeviceInfo.AudioDeviceType int audioDeviceCategory) { 144 mAudioDeviceCategory = audioDeviceCategory; 145 } 146 isBtDeviceCategoryFixed()147 public synchronized boolean isBtDeviceCategoryFixed() { 148 updateAudioDeviceCategory(); 149 return mAutoBtCategorySet; 150 } 151 updateAudioDeviceCategory()152 public synchronized boolean updateAudioDeviceCategory() { 153 if (!isBluetoothDevice(mInternalDeviceType)) { 154 return false; 155 } 156 if (mAutoBtCategorySet) { 157 // no need to update. The auto value is already set. 158 return false; 159 } 160 161 int newAudioDeviceCategory = BtHelper.getBtDeviceCategory(mDeviceAddress); 162 if (newAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_UNKNOWN) { 163 // no info provided by the BtDevice metadata 164 return false; 165 } 166 167 mAudioDeviceCategory = newAudioDeviceCategory; 168 mAutoBtCategorySet = true; 169 return true; 170 171 } 172 173 @Override equals(Object obj)174 public boolean equals(Object obj) { 175 if (this == obj) { 176 return true; 177 } 178 if (obj == null) { 179 return false; 180 } 181 // type check and cast 182 if (getClass() != obj.getClass()) { 183 return false; 184 } 185 final AdiDeviceState sads = (AdiDeviceState) obj; 186 return mDeviceType == sads.mDeviceType 187 && mInternalDeviceType == sads.mInternalDeviceType 188 && mDeviceAddress.equals(sads.mDeviceAddress) // NonNull 189 && mSAEnabled == sads.mSAEnabled 190 && mHasHeadTracker == sads.mHasHeadTracker 191 && mHeadTrackerEnabled == sads.mHeadTrackerEnabled 192 && mAudioDeviceCategory == sads.mAudioDeviceCategory; 193 } 194 195 @Override hashCode()196 public int hashCode() { 197 return Objects.hash(mDeviceType, mInternalDeviceType, mDeviceAddress, mSAEnabled, 198 mHasHeadTracker, mHeadTrackerEnabled, mAudioDeviceCategory); 199 } 200 201 @Override toString()202 public String toString() { 203 return "type: " + mDeviceType 204 + " internal type: 0x" + Integer.toHexString(mInternalDeviceType) 205 + " addr: " + Utils.anonymizeBluetoothAddress(mInternalDeviceType, mDeviceAddress) 206 + " bt audio type: " 207 + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory) 208 + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker 209 + " HTenabled: " + mHeadTrackerEnabled; 210 } 211 toPersistableString()212 public synchronized String toPersistableString() { 213 return (new StringBuilder().append(mDeviceType) 214 .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress) 215 .append(SETTING_FIELD_SEPARATOR).append(mSAEnabled ? "1" : "0") 216 .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0") 217 .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0") 218 .append(SETTING_FIELD_SEPARATOR).append(mInternalDeviceType) 219 .append(SETTING_FIELD_SEPARATOR).append(mAudioDeviceCategory) 220 .toString()); 221 } 222 223 /** 224 * Gets the max size (including separators) when persisting the elements with 225 * {@link AdiDeviceState#toPersistableString()}. 226 */ getPeristedMaxSize()227 public static int getPeristedMaxSize() { 228 return 39; /* (mDeviceType)2 + (mDeviceAddress)17 + (mInternalDeviceType)9 + (mSAEnabled)1 229 + (mHasHeadTracker)1 + (mHasHeadTrackerEnabled)1 230 + (mAudioDeviceCategory)1 + (SETTINGS_FIELD_SEPARATOR)6 231 + (SETTING_DEVICE_SEPARATOR)1 */ 232 } 233 234 @Nullable fromPersistedString(@ullable String persistedString)235 public static AdiDeviceState fromPersistedString(@Nullable String persistedString) { 236 if (persistedString == null) { 237 return null; 238 } 239 if (persistedString.isEmpty()) { 240 return null; 241 } 242 String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR); 243 // we may have 5 fields for the legacy AdiDeviceState and 6 containing the internal 244 // device type 245 if (fields.length < 5 || fields.length > 7) { 246 // different number of fields may mean corruption, ignore those settings 247 // newly added fields are optional (mInternalDeviceType, mBtAudioDeviceCategory) 248 return null; 249 } 250 try { 251 final int deviceType = Integer.parseInt(fields[0]); 252 int internalDeviceType = -1; 253 if (fields.length >= 6) { 254 internalDeviceType = Integer.parseInt(fields[5]); 255 } 256 int audioDeviceCategory = AUDIO_DEVICE_CATEGORY_UNKNOWN; 257 if (fields.length == 7) { 258 audioDeviceCategory = Integer.parseInt(fields[6]); 259 } 260 final AdiDeviceState deviceState = new AdiDeviceState(deviceType, 261 internalDeviceType, fields[1]); 262 deviceState.setSAEnabled(Integer.parseInt(fields[2]) == 1); 263 deviceState.setHasHeadTracker(Integer.parseInt(fields[3]) == 1); 264 deviceState.setHeadTrackerEnabled(Integer.parseInt(fields[4]) == 1); 265 deviceState.setAudioDeviceCategory(audioDeviceCategory); 266 // update in case we can automatically determine the category 267 deviceState.updateAudioDeviceCategory(); 268 return deviceState; 269 } catch (NumberFormatException e) { 270 Log.e(TAG, "unable to parse setting for AdiDeviceState: " + persistedString, e); 271 return null; 272 } 273 } 274 getAudioDeviceAttributes()275 public synchronized AudioDeviceAttributes getAudioDeviceAttributes() { 276 return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, 277 mDeviceType, mDeviceAddress); 278 } 279 } 280