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