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 17 package com.android.settingslib.bluetooth; 18 19 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO; 20 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL; 21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 22 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 23 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothClass; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothHearingAid; 28 import android.bluetooth.BluetoothProfile; 29 import android.content.Context; 30 import android.util.Log; 31 32 import androidx.annotation.IntDef; 33 import androidx.annotation.NonNull; 34 35 import com.android.settingslib.R; 36 import com.android.settingslib.Utils; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.lang.reflect.InvocationTargetException; 41 import java.lang.reflect.Method; 42 import java.util.ArrayList; 43 import java.util.List; 44 45 public class HearingAidProfile implements LocalBluetoothProfile { 46 @Retention(RetentionPolicy.SOURCE) 47 @IntDef({ 48 DeviceSide.SIDE_INVALID, 49 DeviceSide.SIDE_LEFT, 50 DeviceSide.SIDE_RIGHT 51 }) 52 53 /** Side definition for hearing aids. See {@link BluetoothHearingAid}. */ 54 public @interface DeviceSide { 55 int SIDE_INVALID = -1; 56 int SIDE_LEFT = 0; 57 int SIDE_RIGHT = 1; 58 } 59 60 @Retention(RetentionPolicy.SOURCE) 61 @IntDef({ 62 DeviceMode.MODE_INVALID, 63 DeviceMode.MODE_MONAURAL, 64 DeviceMode.MODE_BINAURAL 65 }) 66 67 /** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */ 68 public @interface DeviceMode { 69 int MODE_INVALID = -1; 70 int MODE_MONAURAL = 0; 71 int MODE_BINAURAL = 1; 72 } 73 74 private static final String TAG = "HearingAidProfile"; 75 private static boolean V = true; 76 77 private Context mContext; 78 79 private BluetoothHearingAid mService; 80 private boolean mIsProfileReady; 81 82 private final CachedBluetoothDeviceManager mDeviceManager; 83 84 static final String NAME = "HearingAid"; 85 private final LocalBluetoothProfileManager mProfileManager; 86 private final BluetoothAdapter mBluetoothAdapter; 87 88 // Order of this profile in device profiles list 89 private static final int ORDINAL = 1; 90 91 // These callbacks run on the main thread. 92 private final class HearingAidServiceListener 93 implements BluetoothProfile.ServiceListener { 94 onServiceConnected(int profile, BluetoothProfile proxy)95 public void onServiceConnected(int profile, BluetoothProfile proxy) { 96 mService = (BluetoothHearingAid) proxy; 97 // We just bound to the service, so refresh the UI for any connected HearingAid devices. 98 List<BluetoothDevice> deviceList = mService.getConnectedDevices(); 99 while (!deviceList.isEmpty()) { 100 BluetoothDevice nextDevice = deviceList.remove(0); 101 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); 102 // we may add a new device here, but generally this should not happen 103 if (device == null) { 104 if (V) { 105 Log.d(TAG, "HearingAidProfile found new device: " + nextDevice); 106 } 107 device = mDeviceManager.addDevice(nextDevice); 108 } 109 device.onProfileStateChanged(HearingAidProfile.this, 110 BluetoothProfile.STATE_CONNECTED); 111 device.refresh(); 112 } 113 114 // Check current list of CachedDevices to see if any are Hearing Aid devices. 115 mDeviceManager.updateHearingAidsDevices(); 116 mIsProfileReady=true; 117 mProfileManager.callServiceConnectedListeners(); 118 } 119 onServiceDisconnected(int profile)120 public void onServiceDisconnected(int profile) { 121 mIsProfileReady=false; 122 } 123 } 124 isProfileReady()125 public boolean isProfileReady() { 126 return mIsProfileReady; 127 } 128 129 @Override getProfileId()130 public int getProfileId() { 131 return BluetoothProfile.HEARING_AID; 132 } 133 HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)134 HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager, 135 LocalBluetoothProfileManager profileManager) { 136 mContext = context; 137 mDeviceManager = deviceManager; 138 mProfileManager = profileManager; 139 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 140 mBluetoothAdapter.getProfileProxy(context, 141 new HearingAidServiceListener(), BluetoothProfile.HEARING_AID); 142 } 143 accessProfileEnabled()144 public boolean accessProfileEnabled() { 145 return false; 146 } 147 isAutoConnectable()148 public boolean isAutoConnectable() { 149 return true; 150 } 151 152 /** 153 * Get Hearing Aid devices matching connection states{ 154 * @code BluetoothProfile.STATE_CONNECTED, 155 * @code BluetoothProfile.STATE_CONNECTING, 156 * @code BluetoothProfile.STATE_DISCONNECTING} 157 * 158 * @return Matching device list 159 */ getConnectedDevices()160 public List<BluetoothDevice> getConnectedDevices() { 161 return getDevicesByStates(new int[] { 162 BluetoothProfile.STATE_CONNECTED, 163 BluetoothProfile.STATE_CONNECTING, 164 BluetoothProfile.STATE_DISCONNECTING}); 165 } 166 167 /** 168 * Get Hearing Aid devices matching connection states{ 169 * @code BluetoothProfile.STATE_DISCONNECTED, 170 * @code BluetoothProfile.STATE_CONNECTED, 171 * @code BluetoothProfile.STATE_CONNECTING, 172 * @code BluetoothProfile.STATE_DISCONNECTING} 173 * 174 * @return Matching device list 175 */ getConnectableDevices()176 public List<BluetoothDevice> getConnectableDevices() { 177 return getDevicesByStates(new int[] { 178 BluetoothProfile.STATE_DISCONNECTED, 179 BluetoothProfile.STATE_CONNECTED, 180 BluetoothProfile.STATE_CONNECTING, 181 BluetoothProfile.STATE_DISCONNECTING}); 182 } 183 getDevicesByStates(int[] states)184 private List<BluetoothDevice> getDevicesByStates(int[] states) { 185 if (mService == null) { 186 return new ArrayList<BluetoothDevice>(0); 187 } 188 return mService.getDevicesMatchingConnectionStates(states); 189 } 190 getConnectionStatus(BluetoothDevice device)191 public int getConnectionStatus(BluetoothDevice device) { 192 if (mService == null) { 193 return BluetoothProfile.STATE_DISCONNECTED; 194 } 195 return mService.getConnectionState(device); 196 } 197 setActiveDevice(BluetoothDevice device)198 public boolean setActiveDevice(BluetoothDevice device) { 199 if (mBluetoothAdapter == null) { 200 return false; 201 } 202 int profiles = Utils.isAudioModeOngoingCall(mContext) 203 ? ACTIVE_DEVICE_PHONE_CALL 204 : ACTIVE_DEVICE_AUDIO; 205 return device == null 206 ? mBluetoothAdapter.removeActiveDevice(profiles) 207 : mBluetoothAdapter.setActiveDevice(device, profiles); 208 } 209 getActiveDevices()210 public List<BluetoothDevice> getActiveDevices() { 211 if (mBluetoothAdapter == null) { 212 return new ArrayList<>(); 213 } 214 return mBluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID); 215 } 216 217 @Override isEnabled(BluetoothDevice device)218 public boolean isEnabled(BluetoothDevice device) { 219 if (mService == null || device == null) { 220 return false; 221 } 222 return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN; 223 } 224 225 @Override getConnectionPolicy(BluetoothDevice device)226 public int getConnectionPolicy(BluetoothDevice device) { 227 if (mService == null || device == null) { 228 return CONNECTION_POLICY_FORBIDDEN; 229 } 230 return mService.getConnectionPolicy(device); 231 } 232 233 @Override setEnabled(BluetoothDevice device, boolean enabled)234 public boolean setEnabled(BluetoothDevice device, boolean enabled) { 235 boolean isEnabled = false; 236 if (mService == null || device == null) { 237 return false; 238 } 239 if (enabled) { 240 if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) { 241 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED); 242 } 243 } else { 244 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN); 245 } 246 247 return isEnabled; 248 } 249 250 /** 251 * Tells remote device to set an absolute volume. 252 * 253 * @param volume Absolute volume to be set on remote 254 */ setVolume(int volume)255 public void setVolume(int volume) { 256 if (mService == null) { 257 return; 258 } 259 mService.setVolume(volume); 260 } 261 262 /** 263 * Gets the HiSyncId (unique hearing aid device identifier) of the device. 264 * 265 * @param device Bluetooth device 266 * @return the HiSyncId of the device 267 */ getHiSyncId(BluetoothDevice device)268 public long getHiSyncId(BluetoothDevice device) { 269 if (mService == null || device == null) { 270 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 271 } 272 return mService.getHiSyncId(device); 273 } 274 275 /** 276 * Gets the side of the device. 277 * 278 * @param device Bluetooth device. 279 * @return side of the device. See {@link DeviceSide}. 280 */ 281 @DeviceSide getDeviceSide(@onNull BluetoothDevice device)282 public int getDeviceSide(@NonNull BluetoothDevice device) { 283 final int defaultValue = DeviceSide.SIDE_INVALID; 284 if (mService == null) { 285 Log.w(TAG, "Proxy not attached to HearingAidService"); 286 return defaultValue; 287 } 288 289 try { 290 Method method = mService.getClass().getDeclaredMethod("getDeviceSideInternal", 291 BluetoothDevice.class); 292 method.setAccessible(true); 293 return (int) method.invoke(mService, device); 294 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 295 Log.e(TAG, "fail to get getDeviceSideInternal\n" + e.toString() + "\n" 296 + Log.getStackTraceString(new Throwable())); 297 return defaultValue; 298 } 299 } 300 301 /** 302 * Gets the mode of the device. 303 * 304 * @param device Bluetooth device 305 * @return mode of the device. See {@link DeviceMode}. 306 */ 307 @DeviceMode getDeviceMode(@onNull BluetoothDevice device)308 public int getDeviceMode(@NonNull BluetoothDevice device) { 309 final int defaultValue = DeviceMode.MODE_INVALID; 310 if (mService == null) { 311 Log.w(TAG, "Proxy not attached to HearingAidService"); 312 return defaultValue; 313 } 314 315 try { 316 Method method = mService.getClass().getDeclaredMethod("getDeviceModeInternal", 317 BluetoothDevice.class); 318 method.setAccessible(true); 319 return (int) method.invoke(mService, device); 320 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 321 Log.e(TAG, "fail to get getDeviceModeInternal\n" + e.toString() + "\n" 322 + Log.getStackTraceString(new Throwable())); 323 324 return defaultValue; 325 } 326 } 327 toString()328 public String toString() { 329 return NAME; 330 } 331 getOrdinal()332 public int getOrdinal() { 333 return ORDINAL; 334 } 335 getNameResource(BluetoothDevice device)336 public int getNameResource(BluetoothDevice device) { 337 return R.string.bluetooth_profile_hearing_aid; 338 } 339 getSummaryResourceForDevice(BluetoothDevice device)340 public int getSummaryResourceForDevice(BluetoothDevice device) { 341 int state = getConnectionStatus(device); 342 switch (state) { 343 case BluetoothProfile.STATE_DISCONNECTED: 344 return R.string.bluetooth_hearing_aid_profile_summary_use_for; 345 346 case BluetoothProfile.STATE_CONNECTED: 347 return R.string.bluetooth_hearing_aid_profile_summary_connected; 348 349 default: 350 return BluetoothUtils.getConnectionStateSummary(state); 351 } 352 } 353 getDrawableResource(BluetoothClass btClass)354 public int getDrawableResource(BluetoothClass btClass) { 355 return com.android.internal.R.drawable.ic_bt_hearing_aid; 356 } 357 finalize()358 protected void finalize() { 359 Log.d(TAG, "finalize()"); 360 if (mService != null) { 361 try { 362 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEARING_AID, 363 mService); 364 mService = null; 365 }catch (Throwable t) { 366 Log.w(TAG, "Error cleaning up Hearing Aid proxy", t); 367 } 368 } 369 } 370 } 371