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