1 /*
2  * Copyright (C) 2018 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 package com.android.settingslib.bluetooth;
17 
18 import android.bluetooth.BluetoothDevice;
19 import android.bluetooth.BluetoothHearingAid;
20 import android.bluetooth.BluetoothProfile;
21 import android.util.Log;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Set;
28 
29 /**
30  * HearingAidDeviceManager manages the set of remote HearingAid Bluetooth devices.
31  */
32 public class HearingAidDeviceManager {
33     private static final String TAG = "HearingAidDeviceManager";
34     private static final boolean DEBUG = BluetoothUtils.D;
35 
36     private final LocalBluetoothManager mBtManager;
37     private final List<CachedBluetoothDevice> mCachedDevices;
HearingAidDeviceManager(LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices)38     HearingAidDeviceManager(LocalBluetoothManager localBtManager,
39             List<CachedBluetoothDevice> CachedDevices) {
40         mBtManager = localBtManager;
41         mCachedDevices = CachedDevices;
42     }
43 
initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice)44     void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) {
45         long hiSyncId = getHiSyncId(newDevice.getDevice());
46         if (isValidHiSyncId(hiSyncId)) {
47             // Once hiSyncId is valid, assign hiSyncId
48             newDevice.setHiSyncId(hiSyncId);
49         }
50     }
51 
getHiSyncId(BluetoothDevice device)52     private long getHiSyncId(BluetoothDevice device) {
53         LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
54         HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
55         if (profileProxy != null) {
56             return profileProxy.getHiSyncId(device);
57         }
58         return BluetoothHearingAid.HI_SYNC_ID_INVALID;
59     }
60 
setSubDeviceIfNeeded(CachedBluetoothDevice newDevice)61     boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) {
62         final long hiSyncId = newDevice.getHiSyncId();
63         if (isValidHiSyncId(hiSyncId)) {
64             final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId);
65             // Just add one of the hearing aids from a pair in the list that is shown in the UI.
66             // Once there is another device with the same hiSyncId, to add new device as sub
67             // device.
68             if (hearingAidDevice != null) {
69                 hearingAidDevice.setSubDevice(newDevice);
70                 return true;
71             }
72         }
73         return false;
74     }
75 
isValidHiSyncId(long hiSyncId)76     private boolean isValidHiSyncId(long hiSyncId) {
77         return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
78     }
79 
getCachedDevice(long hiSyncId)80     private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
81         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
82             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
83             if (cachedDevice.getHiSyncId() == hiSyncId) {
84                 return cachedDevice;
85             }
86         }
87         return null;
88     }
89 
90     // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId
updateHearingAidsDevices()91     void updateHearingAidsDevices() {
92         final Set<Long> newSyncIdSet = new HashSet<Long>();
93         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
94             // Do nothing if HiSyncId has been assigned
95             if (!isValidHiSyncId(cachedDevice.getHiSyncId())) {
96                 final long newHiSyncId = getHiSyncId(cachedDevice.getDevice());
97                 // Do nothing if there is no HiSyncId on Bluetooth device
98                 if (isValidHiSyncId(newHiSyncId)) {
99                     cachedDevice.setHiSyncId(newHiSyncId);
100                     newSyncIdSet.add(newHiSyncId);
101                 }
102             }
103         }
104         for (Long syncId : newSyncIdSet) {
105             onHiSyncIdChanged(syncId);
106         }
107     }
108 
109     // Group devices by hiSyncId
110     @VisibleForTesting
onHiSyncIdChanged(long hiSyncId)111     void onHiSyncIdChanged(long hiSyncId) {
112         int firstMatchedIndex = -1;
113 
114         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
115             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
116             if (cachedDevice.getHiSyncId() != hiSyncId) {
117                 continue;
118             }
119             if (firstMatchedIndex == -1) {
120                 // Found the first one
121                 firstMatchedIndex = i;
122                 continue;
123             }
124             // Found the second one
125             int indexToRemoveFromUi;
126             CachedBluetoothDevice subDevice;
127             CachedBluetoothDevice mainDevice;
128             // Since the hiSyncIds have been updated for a connected pair of hearing aids,
129             // we remove the entry of one the hearing aids from the UI. Unless the
130             // hiSyncId get updated, the system does not know it is a hearing aid, so we add
131             // both the hearing aids as separate entries in the UI first, then remove one
132             // of them after the hiSyncId is populated. We will choose the device that
133             // is not connected to be removed.
134             if (cachedDevice.isConnected()) {
135                 mainDevice = cachedDevice;
136                 indexToRemoveFromUi = firstMatchedIndex;
137                 subDevice = mCachedDevices.get(firstMatchedIndex);
138             } else {
139                 mainDevice = mCachedDevices.get(firstMatchedIndex);
140                 indexToRemoveFromUi = i;
141                 subDevice = cachedDevice;
142             }
143 
144             mainDevice.setSubDevice(subDevice);
145             mCachedDevices.remove(indexToRemoveFromUi);
146             log("onHiSyncIdChanged: removed from UI device =" + subDevice
147                     + ", with hiSyncId=" + hiSyncId);
148             mBtManager.getEventManager().dispatchDeviceRemoved(subDevice);
149             break;
150         }
151     }
152 
153     // @return {@code true}, the event is processed inside the method. It is for updating
154     // hearing aid device on main-sub relationship when receiving connected or disconnected.
155     // @return {@code false}, it is not hearing aid device or to process it same as other profiles
onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)156     boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
157             int state) {
158         switch (state) {
159             case BluetoothProfile.STATE_CONNECTED:
160                 onHiSyncIdChanged(cachedDevice.getHiSyncId());
161                 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
162                 if (mainDevice != null){
163                     if (mainDevice.isConnected()) {
164                         // When main device exists and in connected state, receiving sub device
165                         // connection. To refresh main device UI
166                         mainDevice.refresh();
167                         return true;
168                     } else {
169                         // When both Hearing Aid devices are disconnected, receiving sub device
170                         // connection. To switch content and dispatch to notify UI change
171                         mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
172                         mainDevice.switchSubDeviceContent();
173                         mainDevice.refresh();
174                         // It is necessary to do remove and add for updating the mapping on
175                         // preference and device
176                         mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
177                         return true;
178                     }
179                 }
180                 break;
181             case BluetoothProfile.STATE_DISCONNECTED:
182                 mainDevice = findMainDevice(cachedDevice);
183                 if (mainDevice != null) {
184                     // When main device exists, receiving sub device disconnection
185                     // To update main device UI
186                     mainDevice.refresh();
187                     return true;
188                 }
189                 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
190                 if (subDevice != null && subDevice.isConnected()) {
191                     // Main device is disconnected and sub device is connected
192                     // To copy data from sub device to main device
193                     mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
194                     cachedDevice.switchSubDeviceContent();
195                     cachedDevice.refresh();
196                     // It is necessary to do remove and add for updating the mapping on
197                     // preference and device
198                     mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
199                     return true;
200                 }
201                 break;
202         }
203         return false;
204     }
205 
findMainDevice(CachedBluetoothDevice device)206     CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
207         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
208             if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
209                 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
210                 if (subDevice != null && subDevice.equals(device)) {
211                     return cachedDevice;
212                 }
213             }
214         }
215         return null;
216     }
217 
log(String msg)218     private void log(String msg) {
219         if (DEBUG) {
220             Log.d(TAG, msg);
221         }
222     }
223 }