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