1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.M;
4 import static android.os.Build.VERSION_CODES.N;
5 import static android.os.Build.VERSION_CODES.N_MR1;
6 import static android.os.Build.VERSION_CODES.P;
7 import static android.os.Build.VERSION_CODES.Q;
8 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
9 import static org.robolectric.util.ReflectionHelpers.callConstructor;
10 import static org.robolectric.util.ReflectionHelpers.getStaticField;
11 
12 import android.annotation.Nullable;
13 import android.annotation.TargetApi;
14 import android.content.Intent;
15 import android.hardware.usb.UsbAccessory;
16 import android.hardware.usb.UsbDevice;
17 import android.hardware.usb.UsbDeviceConnection;
18 import android.hardware.usb.UsbManager;
19 import android.hardware.usb.UsbPort;
20 import android.hardware.usb.UsbPortStatus;
21 import android.os.ParcelFileDescriptor;
22 import com.google.common.base.Preconditions;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import org.robolectric.RuntimeEnvironment;
29 import org.robolectric.annotation.ClassName;
30 import org.robolectric.annotation.HiddenApi;
31 import org.robolectric.annotation.Implementation;
32 import org.robolectric.annotation.Implements;
33 import org.robolectric.annotation.RealObject;
34 import org.robolectric.annotation.Resetter;
35 import org.robolectric.util.reflector.ForType;
36 
37 /** Robolectric implementation of {@link android.hardware.usb.UsbManager}. */
38 @Implements(value = UsbManager.class)
39 public class ShadowUsbManager {
40 
41   @RealObject private UsbManager realUsbManager;
42 
43   /**
44    * A mapping from the package names to a list of USB devices for which permissions are granted.
45    */
46   private static final HashMap<String, List<UsbDevice>> grantedDevicePermissions = new HashMap<>();
47 
48   /**
49    * A mapping from the package names to a list of USB accessories for which permissions are
50    * granted.
51    */
52   private static final HashMap<String, List<UsbAccessory>> grantedAccessoryPermissions =
53       new HashMap<>();
54 
55   /**
56    * A mapping from the USB device names to the USB device instances.
57    *
58    * @see UsbManager#getDeviceList()
59    */
60   private static final HashMap<String, UsbDevice> usbDevices = new HashMap<>();
61 
62   /** A mapping from USB port ID to the port object. */
63   private static final HashMap<String, UsbPort> usbPorts = new HashMap<>();
64 
65   /** A mapping from USB port to the status of that port. */
66   private static final HashMap<UsbPort, UsbPortStatus> usbPortStatuses = new HashMap<>();
67 
68   private static UsbAccessory attachedUsbAccessory = null;
69 
70   @Resetter
reset()71   public static void reset() {
72     grantedDevicePermissions.clear();
73     grantedAccessoryPermissions.clear();
74     usbDevices.clear();
75     usbPorts.clear();
76     usbPortStatuses.clear();
77     attachedUsbAccessory = null;
78   }
79 
80   /** Returns true if the caller has permission to access the device. */
81   @Implementation
hasPermission(UsbDevice device)82   protected boolean hasPermission(UsbDevice device) {
83     return hasPermissionForPackage(device, RuntimeEnvironment.getApplication().getPackageName());
84   }
85 
86   /** Returns true if the given package has permission to access the device. */
hasPermissionForPackage(UsbDevice device, String packageName)87   public boolean hasPermissionForPackage(UsbDevice device, String packageName) {
88     List<UsbDevice> usbDevices = grantedDevicePermissions.get(packageName);
89     return usbDevices != null && usbDevices.contains(device);
90   }
91 
92   /** Returns true if the caller has permission to access the accessory. */
93   @Implementation
hasPermission(UsbAccessory accessory)94   protected boolean hasPermission(UsbAccessory accessory) {
95     return hasPermissionForPackage(accessory, RuntimeEnvironment.getApplication().getPackageName());
96   }
97 
98   /** Returns true if the given package has permission to access the device. */
hasPermissionForPackage(UsbAccessory accessory, String packageName)99   public boolean hasPermissionForPackage(UsbAccessory accessory, String packageName) {
100     List<UsbAccessory> usbAccessories = grantedAccessoryPermissions.get(packageName);
101     return usbAccessories != null && usbAccessories.contains(accessory);
102   }
103 
104   @Implementation(minSdk = N)
105   @HiddenApi
grantPermission(UsbDevice device)106   protected void grantPermission(UsbDevice device) {
107     grantPermission(device, RuntimeEnvironment.getApplication().getPackageName());
108   }
109 
110   @Implementation(minSdk = N_MR1)
111   @HiddenApi // SystemApi
grantPermission(UsbDevice device, String packageName)112   protected void grantPermission(UsbDevice device, String packageName) {
113     List<UsbDevice> usbDevices = grantedDevicePermissions.get(packageName);
114     if (usbDevices == null) {
115       usbDevices = new ArrayList<>();
116       grantedDevicePermissions.put(packageName, usbDevices);
117     }
118     usbDevices.add(device);
119   }
120 
121   /** Grants permission for the accessory. */
grantPermission(UsbAccessory accessory)122   public void grantPermission(UsbAccessory accessory) {
123     String packageName = RuntimeEnvironment.getApplication().getPackageName();
124     List<UsbAccessory> usbAccessories = grantedAccessoryPermissions.get(packageName);
125     if (usbAccessories == null) {
126       usbAccessories = new ArrayList<>();
127       grantedAccessoryPermissions.put(packageName, usbAccessories);
128     }
129     usbAccessories.add(accessory);
130   }
131 
132   /**
133    * Revokes permission to a USB device granted to a package. This method does nothing if the
134    * package doesn't have permission to access the device.
135    */
revokePermission(UsbDevice device, String packageName)136   public void revokePermission(UsbDevice device, String packageName) {
137     List<UsbDevice> usbDevices = grantedDevicePermissions.get(packageName);
138     if (usbDevices != null) {
139       usbDevices.remove(device);
140     }
141   }
142 
143   /**
144    * Revokes permission to a USB accessory granted to a package. This method does nothing if the
145    * package doesn't have permission to access the accessory.
146    */
revokePermission(UsbAccessory accessory, String packageName)147   public void revokePermission(UsbAccessory accessory, String packageName) {
148     List<UsbAccessory> usbAccessories = grantedAccessoryPermissions.get(packageName);
149     if (usbAccessories != null) {
150       usbAccessories.remove(accessory);
151     }
152   }
153 
154   /**
155    * Returns a HashMap containing all USB devices currently attached. USB device name is the key for
156    * the returned HashMap. The result will be empty if no devices are attached, or if USB host mode
157    * is inactive or unsupported.
158    */
159   @Implementation
getDeviceList()160   protected HashMap<String, UsbDevice> getDeviceList() {
161     return new HashMap<>(usbDevices);
162   }
163 
164   @Implementation
getAccessoryList()165   protected UsbAccessory[] getAccessoryList() {
166     // Currently Android only supports having a single accessory attached, and if nothing
167     // is attached, this method actually returns null in the real implementation.
168     if (attachedUsbAccessory == null) {
169       return null;
170     }
171 
172     return new UsbAccessory[] {attachedUsbAccessory};
173   }
174 
175   /** Sets the currently attached Usb accessory returned in #getAccessoryList. */
setAttachedUsbAccessory(UsbAccessory usbAccessory)176   public void setAttachedUsbAccessory(UsbAccessory usbAccessory) {
177     this.attachedUsbAccessory = usbAccessory;
178   }
179 
180   /**
181    * Adds a USB device into available USB devices map with permission value. If the USB device
182    * already exists, updates the USB device with new permission value.
183    */
addOrUpdateUsbDevice(UsbDevice usbDevice, boolean hasPermission)184   public void addOrUpdateUsbDevice(UsbDevice usbDevice, boolean hasPermission) {
185     Preconditions.checkNotNull(usbDevice);
186     Preconditions.checkNotNull(usbDevice.getDeviceName());
187     usbDevices.put(usbDevice.getDeviceName(), usbDevice);
188     if (hasPermission) {
189       grantPermission(usbDevice);
190     } else {
191       revokePermission(usbDevice, RuntimeEnvironment.getApplication().getPackageName());
192     }
193   }
194 
195   /** Removes a USB device from available USB devices map. */
removeUsbDevice(UsbDevice usbDevice)196   public void removeUsbDevice(UsbDevice usbDevice) {
197     Preconditions.checkNotNull(usbDevice);
198     usbDevices.remove(usbDevice.getDeviceName());
199     revokePermission(usbDevice, RuntimeEnvironment.getApplication().getPackageName());
200   }
201 
202   @Implementation(minSdk = M, maxSdk = P)
203   @HiddenApi
getPorts()204   protected @ClassName("android.hardware.usb.UsbPort[]") Object getPorts() {
205     return usbPortStatuses.keySet().toArray(new UsbPort[usbPortStatuses.size()]);
206   }
207 
208   @Implementation(minSdk = Q, methodName = "getPorts")
209   @HiddenApi
getPortsFromQ()210   protected List</*android.hardware.usb.UsbPort*/ ?> getPortsFromQ() {
211     return new ArrayList<>(usbPortStatuses.keySet());
212   }
213 
214   /** Remove all added ports from UsbManager. */
clearPorts()215   public void clearPorts() {
216     usbPorts.clear();
217     usbPortStatuses.clear();
218   }
219 
220   /** Adds a USB port with given ID to UsbManager. */
addPort(String portId)221   public void addPort(String portId) {
222     if (RuntimeEnvironment.getApiLevel() >= Q) {
223       addPort(
224           portId,
225           UsbPortStatus.MODE_DUAL,
226           UsbPortStatus.POWER_ROLE_SINK,
227           UsbPortStatus.DATA_ROLE_DEVICE,
228           0);
229       return;
230     }
231 
232     UsbPort usbPort =
233         callConstructor(
234             UsbPort.class,
235             from(String.class, portId),
236             from(int.class, getStaticField(UsbPort.class, "MODE_DUAL")));
237     usbPorts.put(portId, usbPort);
238     usbPortStatuses.put(
239         usbPort,
240         (UsbPortStatus)
241             createUsbPortStatus(
242                 getStaticField(UsbPort.class, "MODE_DUAL"),
243                 getStaticField(UsbPort.class, "POWER_ROLE_SINK"),
244                 getStaticField(UsbPort.class, "DATA_ROLE_DEVICE"),
245                 0));
246   }
247 
248   /** Adds a USB port with given ID and {@link UsbPortStatus} parameters to UsbManager for Q+. */
249   @TargetApi(Q)
addPort( String portId, int statusCurrentMode, int statusCurrentPowerRole, int statusCurrentDataRole, int statusSupportedRoleCombinations)250   public void addPort(
251       String portId,
252       int statusCurrentMode,
253       int statusCurrentPowerRole,
254       int statusCurrentDataRole,
255       int statusSupportedRoleCombinations) {
256     Preconditions.checkState(RuntimeEnvironment.getApiLevel() >= Q);
257     UsbPort usbPort = (UsbPort) createUsbPort(realUsbManager, portId, statusCurrentMode);
258     usbPorts.put(portId, usbPort);
259     usbPortStatuses.put(
260         usbPort,
261         (UsbPortStatus)
262             createUsbPortStatus(
263                 statusCurrentMode,
264                 statusCurrentPowerRole,
265                 statusCurrentDataRole,
266                 statusSupportedRoleCombinations));
267   }
268 
269   /**
270    * Returns the {@link UsbPortStatus} corresponding to the {@link UsbPort} with given {@code
271    * portId} if present; otherwise returns {@code null}.
272    */
273   @Nullable
getPortStatus(String portId)274   public /* UsbPortStatus */ Object getPortStatus(String portId) {
275     return usbPortStatuses.get(usbPorts.get(portId));
276   }
277 
278   @Implementation(minSdk = M)
279   @HiddenApi
getPortStatus( @lassName"android.hardware.usb.UsbPort") Object port)280   protected @ClassName("android.hardware.usb.UsbPortStatus") Object getPortStatus(
281       @ClassName("android.hardware.usb.UsbPort") Object port) {
282     return usbPortStatuses.get(port);
283   }
284 
285   @Implementation(minSdk = M)
286   @HiddenApi
setPortRoles( @lassName"android.hardware.usb.UsbPort") Object port, int powerRole, int dataRole)287   protected void setPortRoles(
288       @ClassName("android.hardware.usb.UsbPort") Object port, int powerRole, int dataRole) {
289     UsbPortStatus status = usbPortStatuses.get(port);
290     usbPortStatuses.put(
291         (UsbPort) port,
292         (UsbPortStatus)
293             createUsbPortStatus(
294                 status.getCurrentMode(),
295                 powerRole,
296                 dataRole,
297                 status.getSupportedRoleCombinations()));
298     RuntimeEnvironment.getApplication()
299         .sendBroadcast(new Intent(UsbManager.ACTION_USB_PORT_CHANGED));
300   }
301 
302   /** Opens a file descriptor from a temporary file. */
303   @Implementation
openDevice(UsbDevice device)304   protected UsbDeviceConnection openDevice(UsbDevice device) {
305     return createUsbDeviceConnection(device);
306   }
307 
308   /** Opens a file descriptor from a temporary file. */
309   @Implementation
openAccessory(UsbAccessory accessory)310   protected ParcelFileDescriptor openAccessory(UsbAccessory accessory) {
311     try {
312       File tmpUsbDir =
313           RuntimeEnvironment.getTempDirectory().createIfNotExists("usb-accessory").toFile();
314       return ParcelFileDescriptor.open(
315           new File(tmpUsbDir, "usb-accessory-file"), ParcelFileDescriptor.MODE_READ_WRITE);
316     } catch (FileNotFoundException error) {
317       throw new RuntimeException("Error shadowing openAccessory", error);
318     }
319   }
320 
321   /**
322    * Helper method for creating a {@link UsbPortStatus}.
323    *
324    * <p>Returns Object to avoid referencing the API M+ UsbPortStatus when running on older
325    * platforms.
326    */
createUsbPortStatus( int currentMode, int currentPowerRole, int currentDataRole, int supportedRoleCombinations)327   private static Object createUsbPortStatus(
328       int currentMode, int currentPowerRole, int currentDataRole, int supportedRoleCombinations) {
329     if (RuntimeEnvironment.getApiLevel() >= Q) {
330       return new UsbPortStatus(
331           currentMode, currentPowerRole, currentDataRole, supportedRoleCombinations, 0, 0);
332     }
333     return callConstructor(
334         UsbPortStatus.class,
335         from(int.class, currentMode),
336         from(int.class, currentPowerRole),
337         from(int.class, currentDataRole),
338         from(int.class, supportedRoleCombinations));
339   }
340 
341   /**
342    * Helper method for creating a {@link UsbPort}.
343    *
344    * <p>Returns Object to avoid referencing the API M+ UsbPort when running on older platforms.
345    */
createUsbPort(UsbManager usbManager, String id, int supportedModes)346   private static Object createUsbPort(UsbManager usbManager, String id, int supportedModes) {
347     if (RuntimeEnvironment.getApiLevel() >= Q) {
348       return new UsbPort(usbManager, id, supportedModes, 0, false, false);
349     }
350     return callConstructor(
351         UsbPort.class,
352         from(UsbManager.class, usbManager),
353         from(String.class, id),
354         from(int.class, supportedModes));
355   }
356 
357   /** Helper method for creating a {@link UsbDeviceConnection}. */
createUsbDeviceConnection(UsbDevice device)358   private static UsbDeviceConnection createUsbDeviceConnection(UsbDevice device) {
359     return callConstructor(UsbDeviceConnection.class, from(UsbDevice.class, device));
360   }
361 
362   /** Accessor interface for {@link UsbManager}'s internals. */
363   @ForType(UsbManager.class)
364   public interface _UsbManager_ {
365 
getPorts()366     UsbPort[] getPorts();
367 
getPortStatus(UsbPort port)368     UsbPortStatus getPortStatus(UsbPort port);
369 
setPortRoles(UsbPort port, int powerRole, int dataRole)370     void setPortRoles(UsbPort port, int powerRole, int dataRole);
371   }
372 
373   /** Accessor interface for {@link UsbManager}'s internals (Q+). */
374   @ForType(UsbManager.class)
375   public interface _UsbManagerQ_ {
376 
getPorts()377     List<UsbPort> getPorts();
378   }
379 }
380