1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT; 4 import static android.os.Build.VERSION_CODES.P; 5 import static android.os.Build.VERSION_CODES.S; 6 import static java.util.stream.Collectors.toCollection; 7 8 import android.bluetooth.BluetoothDevice; 9 import android.bluetooth.BluetoothHeadset; 10 import android.bluetooth.BluetoothProfile; 11 import android.content.Intent; 12 import java.util.ArrayList; 13 import java.util.HashMap; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Map.Entry; 17 import java.util.Objects; 18 import javax.annotation.Nullable; 19 import javax.annotation.concurrent.NotThreadSafe; 20 import org.robolectric.RuntimeEnvironment; 21 import org.robolectric.annotation.Implementation; 22 import org.robolectric.annotation.Implements; 23 24 /** Shadow for {@link BluetoothHeadset} */ 25 @NotThreadSafe 26 @Implements(value = BluetoothHeadset.class) 27 public class ShadowBluetoothHeadset { 28 29 private final Map<BluetoothDevice, Integer> bluetoothDevices = new HashMap<>(); 30 private boolean allowsSendVendorSpecificResultCode = true; 31 private BluetoothDevice activeBluetoothDevice; 32 private boolean isVoiceRecognitionSupported = true; 33 34 /** 35 * Overrides behavior of {@link getConnectedDevices}. Returns list of devices that is set up by 36 * call(s) to {@link ShadowBluetoothHeadset#addConnectedDevice}. Returns an empty list by default. 37 */ 38 @Implementation getConnectedDevices()39 protected List<BluetoothDevice> getConnectedDevices() { 40 return bluetoothDevices.entrySet().stream() 41 .filter(entry -> entry.getValue() == BluetoothProfile.STATE_CONNECTED) 42 .map(Entry::getKey) 43 .collect(toCollection(ArrayList::new)); 44 } 45 46 /** Adds the given BluetoothDevice to the shadow's list of "connected devices" */ addConnectedDevice(BluetoothDevice device)47 public void addConnectedDevice(BluetoothDevice device) { 48 addDevice(device, BluetoothProfile.STATE_CONNECTED); 49 } 50 51 /** 52 * Adds the provided BluetoothDevice to the shadow profile's device list with an associated 53 * connectionState. The provided connection state will be returned by {@link 54 * ShadowBluetoothHeadset#getConnectionState}. 55 */ addDevice(BluetoothDevice bluetoothDevice, int connectionState)56 public void addDevice(BluetoothDevice bluetoothDevice, int connectionState) { 57 bluetoothDevices.put(bluetoothDevice, connectionState); 58 } 59 60 /** Remove the given BluetoothDevice from the shadow profile's device list */ removeDevice(BluetoothDevice bluetoothDevice)61 public void removeDevice(BluetoothDevice bluetoothDevice) { 62 bluetoothDevices.remove(bluetoothDevice); 63 } 64 65 /** 66 * Overrides behavior of {@link getConnectionState}. 67 * 68 * @return {@code BluetoothProfile.STATE_CONNECTED} if the given device has been previously added 69 * by a call to {@link ShadowBluetoothHeadset#addConnectedDevice}, and {@code 70 * BluetoothProfile.STATE_DISCONNECTED} otherwise. 71 */ 72 @Implementation getConnectionState(BluetoothDevice device)73 protected int getConnectionState(BluetoothDevice device) { 74 return bluetoothDevices.getOrDefault(device, BluetoothProfile.STATE_DISCONNECTED); 75 } 76 77 /** 78 * Overrides behavior of {@link startVoiceRecognition}. Returns false if 'bluetoothDevice' is null 79 * or voice recognition is already started. Users can listen to {@link 80 * ACTION_AUDIO_STATE_CHANGED}. If this function returns true, this intent will be broadcasted 81 * once with {@link BluetoothProfile.EXTRA_STATE} set to {@link STATE_AUDIO_CONNECTING} and once 82 * set to {@link STATE_AUDIO_CONNECTED}. 83 */ 84 @Implementation startVoiceRecognition(BluetoothDevice bluetoothDevice)85 protected boolean startVoiceRecognition(BluetoothDevice bluetoothDevice) { 86 if (bluetoothDevice == null || !getConnectedDevices().contains(bluetoothDevice)) { 87 return false; 88 } 89 if (activeBluetoothDevice != null) { 90 stopVoiceRecognition(activeBluetoothDevice); 91 return false; 92 } 93 sendAudioStateChangedBroadcast(BluetoothHeadset.STATE_AUDIO_CONNECTING, bluetoothDevice); 94 sendAudioStateChangedBroadcast(BluetoothHeadset.STATE_AUDIO_CONNECTED, bluetoothDevice); 95 96 activeBluetoothDevice = bluetoothDevice; 97 return true; 98 } 99 100 /** 101 * Overrides the behavior of {@link stopVoiceRecognition}. Returns false if voice recognition was 102 * not started or voice recogntion has already ended on this headset. If this function returns 103 * true, {@link ACTION_AUDIO_STATE_CHANGED} intent is broadcasted with {@link 104 * BluetoothProfile.EXTRA_STATE} set to {@link STATE_DISCONNECTED}. 105 */ 106 @Implementation stopVoiceRecognition(BluetoothDevice bluetoothDevice)107 protected boolean stopVoiceRecognition(BluetoothDevice bluetoothDevice) { 108 boolean isDeviceActive = isDeviceActive(bluetoothDevice); 109 activeBluetoothDevice = null; 110 if (isDeviceActive) { 111 sendAudioStateChangedBroadcast(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, bluetoothDevice); 112 } 113 return isDeviceActive; 114 } 115 116 @Implementation isAudioConnected(BluetoothDevice bluetoothDevice)117 protected boolean isAudioConnected(BluetoothDevice bluetoothDevice) { 118 return isDeviceActive(bluetoothDevice); 119 } 120 121 /** 122 * Overrides behavior of {@link sendVendorSpecificResultCode}. 123 * 124 * @return 'true' only if the given device has been previously added by a call to {@link 125 * ShadowBluetoothHeadset#addConnectedDevice} and {@link 126 * ShadowBluetoothHeadset#setAllowsSendVendorSpecificResultCode} has not been called with 127 * 'false' argument. 128 * @throws IllegalArgumentException if 'command' argument is null, per Android API 129 */ 130 @Implementation(minSdk = KITKAT) sendVendorSpecificResultCode( BluetoothDevice device, String command, String arg)131 protected boolean sendVendorSpecificResultCode( 132 BluetoothDevice device, String command, String arg) { 133 if (command == null) { 134 throw new IllegalArgumentException("Command cannot be null"); 135 } 136 return allowsSendVendorSpecificResultCode && getConnectedDevices().contains(device); 137 } 138 139 @Nullable 140 @Implementation(minSdk = P) getActiveDevice()141 protected BluetoothDevice getActiveDevice() { 142 return activeBluetoothDevice; 143 } 144 145 @Implementation(minSdk = P) setActiveDevice(@ullable BluetoothDevice bluetoothDevice)146 protected boolean setActiveDevice(@Nullable BluetoothDevice bluetoothDevice) { 147 activeBluetoothDevice = bluetoothDevice; 148 Intent intent = new Intent(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED); 149 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, activeBluetoothDevice); 150 RuntimeEnvironment.getApplication().sendBroadcast(intent); 151 return true; 152 } 153 154 /** 155 * Sets whether the headset supports voice recognition. 156 * 157 * <p>By default voice recognition is supported. 158 * 159 * @see #isVoiceRecognitionSupported(BluetoothDevice) 160 */ setVoiceRecognitionSupported(boolean supported)161 public void setVoiceRecognitionSupported(boolean supported) { 162 isVoiceRecognitionSupported = supported; 163 } 164 165 /** 166 * Checks whether the headset supports voice recognition. 167 * 168 * @see #setVoiceRecognitionSupported(boolean) 169 */ 170 @Implementation(minSdk = S) isVoiceRecognitionSupported(BluetoothDevice device)171 protected boolean isVoiceRecognitionSupported(BluetoothDevice device) { 172 return isVoiceRecognitionSupported; 173 } 174 175 /** 176 * Affects the behavior of {@link BluetoothHeadset#sendVendorSpecificResultCode} 177 * 178 * @param allowsSendVendorSpecificResultCode can be set to 'false' to simulate the situation where 179 * the system is unable to send vendor-specific result codes to a device 180 */ setAllowsSendVendorSpecificResultCode(boolean allowsSendVendorSpecificResultCode)181 public void setAllowsSendVendorSpecificResultCode(boolean allowsSendVendorSpecificResultCode) { 182 this.allowsSendVendorSpecificResultCode = allowsSendVendorSpecificResultCode; 183 } 184 isDeviceActive(BluetoothDevice bluetoothDevice)185 private boolean isDeviceActive(BluetoothDevice bluetoothDevice) { 186 return Objects.equals(activeBluetoothDevice, bluetoothDevice); 187 } 188 sendAudioStateChangedBroadcast( int bluetoothProfileExtraState, BluetoothDevice bluetoothDevice)189 private static void sendAudioStateChangedBroadcast( 190 int bluetoothProfileExtraState, BluetoothDevice bluetoothDevice) { 191 Intent connectedIntent = 192 new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED) 193 .putExtra(BluetoothProfile.EXTRA_STATE, bluetoothProfileExtraState) 194 .putExtra(BluetoothDevice.EXTRA_DEVICE, bluetoothDevice); 195 196 RuntimeEnvironment.getApplication().sendBroadcast(connectedIntent); 197 } 198 } 199