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 org.robolectric.RuntimeEnvironment.application; 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.content.Intent; 13 import android.hardware.usb.UsbAccessory; 14 import android.hardware.usb.UsbDevice; 15 import android.hardware.usb.UsbManager; 16 import android.hardware.usb.UsbPort; 17 import android.hardware.usb.UsbPortStatus; 18 import android.os.Build; 19 import android.os.ParcelFileDescriptor; 20 import com.google.common.base.Preconditions; 21 import java.io.File; 22 import java.io.FileNotFoundException; 23 import java.util.ArrayList; 24 import java.util.HashMap; 25 import java.util.List; 26 import org.robolectric.RuntimeEnvironment; 27 import org.robolectric.annotation.HiddenApi; 28 import org.robolectric.annotation.Implementation; 29 import org.robolectric.annotation.Implements; 30 import org.robolectric.annotation.RealObject; 31 32 /** Robolectric implementation of {@link android.hardware.usb.UsbManager}. */ 33 @Implements(value = UsbManager.class, looseSignatures = true) 34 public class ShadowUsbManager { 35 36 @RealObject 37 private UsbManager realUsbManager; 38 39 /** 40 * A mapping from the package names to a list of USB devices for which permissions are granted. 41 */ 42 private final HashMap<String, List<UsbDevice>> grantedPermissions = new HashMap<>(); 43 44 /** 45 * A mapping from the USB device names to the USB device instances. 46 * 47 * @see UsbManager#getDeviceList() 48 */ 49 private final HashMap<String, UsbDevice> usbDevices = new HashMap<>(); 50 51 /** A mapping from USB port to the status of that port. */ 52 private final HashMap<UsbPort, UsbPortStatus> usbPorts = new HashMap<>(); 53 54 private UsbAccessory attachedUsbAccessory = null; 55 56 /** Returns true if the caller has permission to access the device. */ 57 @Implementation 58 protected boolean hasPermission(UsbDevice device) { 59 return hasPermissionForPackage(device, RuntimeEnvironment.application.getPackageName()); 60 } 61 62 /** Returns true if the given package has permission to access the device. */ 63 public boolean hasPermissionForPackage(UsbDevice device, String packageName) { 64 List<UsbDevice> usbDevices = grantedPermissions.get(packageName); 65 return usbDevices != null && usbDevices.contains(device); 66 } 67 68 @Implementation(minSdk = N) 69 @HiddenApi 70 protected void grantPermission(UsbDevice device) { 71 grantPermission(device, RuntimeEnvironment.application.getPackageName()); 72 } 73 74 @Implementation(minSdk = N_MR1) 75 @HiddenApi // SystemApi 76 protected void grantPermission(UsbDevice device, String packageName) { 77 List<UsbDevice> usbDevices = grantedPermissions.get(packageName); 78 if (usbDevices == null) { 79 usbDevices = new ArrayList<>(); 80 grantedPermissions.put(packageName, usbDevices); 81 } 82 usbDevices.add(device); 83 } 84 85 /** 86 * Revokes permission to a USB device granted to a package. This method does nothing if the 87 * package doesn't have permission to access the device. 88 */ 89 public void revokePermission(UsbDevice device, String packageName) { 90 List<UsbDevice> usbDevices = grantedPermissions.get(packageName); 91 if (usbDevices != null) { 92 usbDevices.remove(device); 93 } 94 } 95 96 /** 97 * Returns a HashMap containing all USB devices currently attached. USB device name is the key for 98 * the returned HashMap. The result will be empty if no devices are attached, or if USB host mode 99 * is inactive or unsupported. 100 */ 101 @Implementation 102 protected HashMap<String, UsbDevice> getDeviceList() { 103 return new HashMap<>(usbDevices); 104 } 105 106 @Implementation 107 protected UsbAccessory[] getAccessoryList() { 108 // Currently Android only supports having a single accessory attached, and if nothing 109 // is attached, this method actually returns null in the real implementation. 110 if (attachedUsbAccessory == null) { 111 return null; 112 } 113 114 return new UsbAccessory[] {attachedUsbAccessory}; 115 } 116 117 /** Sets the currently attached Usb accessory returned in #getAccessoryList. */ 118 public void setAttachedUsbAccessory(UsbAccessory usbAccessory) { 119 this.attachedUsbAccessory = usbAccessory; 120 } 121 122 /** 123 * Adds a USB device into available USB devices map with permission value. If the USB device 124 * already exists, updates the USB device with new permission value. 125 */ 126 public void addOrUpdateUsbDevice(UsbDevice usbDevice, boolean hasPermission) { 127 Preconditions.checkNotNull(usbDevice); 128 Preconditions.checkNotNull(usbDevice.getDeviceName()); 129 usbDevices.put(usbDevice.getDeviceName(), usbDevice); 130 if (hasPermission) { 131 grantPermission(usbDevice); 132 } else { 133 revokePermission(usbDevice, RuntimeEnvironment.application.getPackageName()); 134 } 135 } 136 137 /** Removes a USB device from available USB devices map. */ 138 public void removeUsbDevice(UsbDevice usbDevice) { 139 Preconditions.checkNotNull(usbDevice); 140 usbDevices.remove(usbDevice.getDeviceName()); 141 revokePermission(usbDevice, RuntimeEnvironment.application.getPackageName()); 142 } 143 144 @Implementation(minSdk = M) 145 @HiddenApi 146 protected /* UsbPort[] */ Object getPorts() { 147 // BEGIN-INTERNAL 148 if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { 149 return new ArrayList<>(usbPorts.keySet()); 150 } 151 152 // END-INTERNAL 153 return usbPorts.keySet().toArray(new UsbPort[usbPorts.size()]); 154 } 155 156 /** Remove all added ports from UsbManager. */ 157 public void clearPorts() { 158 usbPorts.clear(); 159 } 160 161 /** Adds a USB port to UsbManager. */ 162 public void addPort(String portId) { 163 // BEGIN-INTERNAL 164 if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { 165 usbPorts.put( 166 (UsbPort) createUsbPort(realUsbManager, portId, UsbPortStatus.MODE_DUAL), 167 (UsbPortStatus) createUsbPortStatus( 168 UsbPortStatus.MODE_DUAL, 169 UsbPortStatus.POWER_ROLE_SINK, 170 UsbPortStatus.DATA_ROLE_DEVICE, 171 0)); 172 return; 173 } 174 175 // END-INTERNAL 176 usbPorts.put( 177 callConstructor(UsbPort.class, 178 from(String.class, portId), 179 from(int.class, getStaticField(UsbPort.class, "MODE_DUAL"))), 180 (UsbPortStatus) createUsbPortStatus( 181 getStaticField(UsbPort.class, "MODE_DUAL"), 182 getStaticField(UsbPort.class, "POWER_ROLE_SINK"), 183 getStaticField(UsbPort.class, "DATA_ROLE_DEVICE"), 184 0)); 185 } 186 187 @Implementation(minSdk = M) 188 @HiddenApi 189 protected /* UsbPortStatus */ Object getPortStatus(/* UsbPort */ Object port) { 190 return usbPorts.get(port); 191 } 192 193 @Implementation(minSdk = M) 194 @HiddenApi 195 protected void setPortRoles( 196 /* UsbPort */ Object port, /* int */ Object powerRole, /* int */ Object dataRole) { 197 UsbPortStatus status = usbPorts.get(port); 198 usbPorts.put( 199 (UsbPort) port, 200 (UsbPortStatus) createUsbPortStatus( 201 status.getCurrentMode(), 202 (int) powerRole, 203 (int) dataRole, 204 status.getSupportedRoleCombinations())); 205 application.sendBroadcast(new Intent(UsbManager.ACTION_USB_PORT_CHANGED)); 206 } 207 208 /** Opens a file descriptor from a temporary file. */ 209 @Implementation 210 protected ParcelFileDescriptor openAccessory(UsbAccessory accessory) { 211 try { 212 File tmpUsbDir = 213 RuntimeEnvironment.getTempDirectory().createIfNotExists("usb-accessory").toFile(); 214 return ParcelFileDescriptor.open( 215 new File(tmpUsbDir, "usb-accessory-file"), ParcelFileDescriptor.MODE_READ_WRITE); 216 } catch (FileNotFoundException error) { 217 throw new RuntimeException("Error shadowing openAccessory", error); 218 } 219 } 220 221 /** 222 * Helper method for creating a {@link UsbPortStatus}. 223 * 224 * Returns Object to avoid referencing the API M+ UsbPortStatus when running on older platforms. 225 */ 226 private static Object createUsbPortStatus( 227 int currentMode, 228 int currentPowerRole, 229 int currentDataRole, 230 int supportedRoleCombinations) { 231 if (RuntimeEnvironment.getApiLevel() <= P) { 232 return callConstructor(UsbPortStatus.class, 233 from(int.class, currentMode), 234 from(int.class, currentPowerRole), 235 from(int.class, currentDataRole), 236 from(int.class, supportedRoleCombinations)); 237 } 238 // BEGIN-INTERNAL 239 return new UsbPortStatus( 240 currentMode, 241 currentPowerRole, 242 currentDataRole, 243 supportedRoleCombinations, 244 0, 245 0 246 ); 247 // END-INTERNAL 248 } 249 250 /** 251 * Helper method for creating a {@link UsbPort}. 252 * 253 * Returns Object to avoid referencing the API M+ UsbPort when running on older platforms. 254 */ 255 private static Object createUsbPort( 256 UsbManager usbManager, 257 String id, 258 int supportedModes) { 259 if (RuntimeEnvironment.getApiLevel() <= P) { 260 return callConstructor(UsbPort.class, 261 from(UsbManager.class, usbManager), 262 from(String.class, id), 263 from(int.class, supportedModes)); 264 } 265 // BEGIN-INTERNAL 266 return new UsbPort( 267 usbManager, 268 id, 269 supportedModes, 270 0, 271 false, 272 false 273 ); 274 // END-INTERNAL 275 } 276 277 } 278