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