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