• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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