• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static org.robolectric.Shadows.shadowOf;
4 import static org.robolectric.util.reflector.Reflector.reflector;
5 
6 import android.content.Context;
7 import android.hardware.camera2.CameraAccessException;
8 import android.hardware.camera2.CameraCharacteristics;
9 import android.hardware.camera2.CameraDevice;
10 import android.hardware.camera2.CameraDevice.StateCallback;
11 import android.hardware.camera2.CameraManager;
12 import android.hardware.camera2.impl.CameraDeviceImpl;
13 import android.os.Build;
14 import android.os.Build.VERSION_CODES;
15 import android.os.Handler;
16 import com.google.common.base.Preconditions;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.LinkedHashMap;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.WeakHashMap;
24 import java.util.concurrent.Executor;
25 import javax.annotation.Nonnull;
26 import javax.annotation.Nullable;
27 import org.robolectric.RuntimeEnvironment;
28 import org.robolectric.annotation.Implementation;
29 import org.robolectric.annotation.Implements;
30 import org.robolectric.annotation.InDevelopment;
31 import org.robolectric.annotation.RealObject;
32 import org.robolectric.annotation.Resetter;
33 import org.robolectric.util.ReflectionHelpers;
34 import org.robolectric.util.ReflectionHelpers.ClassParameter;
35 import org.robolectric.util.reflector.Accessor;
36 import org.robolectric.util.reflector.Constructor;
37 import org.robolectric.util.reflector.ForType;
38 import org.robolectric.util.reflector.WithType;
39 import org.robolectric.versioning.AndroidVersions.Baklava;
40 import org.robolectric.versioning.AndroidVersions.U;
41 import org.robolectric.versioning.AndroidVersions.V;
42 
43 /** Shadow class for {@link CameraManager} */
44 @Implements(value = CameraManager.class)
45 public class ShadowCameraManager {
46   // Keep references to cameras so they can be closed after each test
47   protected static final Set<CameraDeviceImpl> createdCameras =
48       Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
49   // LinkedHashMap used to ensure getCameraIdList returns ids in the order in which they were added
50   private static final Map<String, CameraCharacteristics> cameraIdToCharacteristics =
51       new LinkedHashMap<>();
52   private static final Map<String, Boolean> cameraTorches = new HashMap<>();
53   private static final Set<CameraManager.AvailabilityCallback> registeredCallbacks =
54       new HashSet<>();
55   // Cannot reference the torch callback in < Android M
56   private static final Set<Object> torchCallbacks = new HashSet<>();
57   // Most recent camera device opened with openCamera
58   private static CameraDevice lastDevice;
59   // Most recent callback passed to openCamera
60   private static CameraDevice.StateCallback lastCallback;
61   @Nullable private static Executor lastCallbackExecutor;
62   @Nullable private static Handler lastCallbackHandler;
63   @RealObject private CameraManager realObject;
64 
65   @Resetter
reset()66   public static void reset() {
67     for (CameraDeviceImpl cameraDevice : createdCameras) {
68       if (cameraDevice != null) {
69         cameraDevice.close();
70       }
71     }
72     createdCameras.clear();
73     cameraIdToCharacteristics.clear();
74     cameraTorches.clear();
75     registeredCallbacks.clear();
76     torchCallbacks.clear();
77     if (lastDevice != null) {
78       lastDevice.close();
79     }
80     lastDevice = null;
81     lastCallback = null;
82     lastCallbackExecutor = null;
83     if (lastCallbackHandler != null) {
84       // Flush existing handler tasks to ensure camera related callbacks are called properly.
85       shadowOf(lastCallbackHandler.getLooper()).idle();
86       lastCallbackHandler.removeCallbacksAndMessages(null);
87     }
88     lastCallbackHandler = null;
89   }
90 
91   @Implementation
92   @Nonnull
getCameraIdList()93   protected String[] getCameraIdList() throws CameraAccessException {
94     Set<String> cameraIds = cameraIdToCharacteristics.keySet();
95     return cameraIds.toArray(new String[0]);
96   }
97 
98   @Implementation
99   @Nonnull
getCameraCharacteristics(@onnull String cameraId)100   protected CameraCharacteristics getCameraCharacteristics(@Nonnull String cameraId) {
101     Preconditions.checkNotNull(cameraId);
102     CameraCharacteristics characteristics = cameraIdToCharacteristics.get(cameraId);
103     Preconditions.checkArgument(characteristics != null);
104     return characteristics;
105   }
106 
107   @Implementation(minSdk = VERSION_CODES.M)
setTorchMode(@onnull String cameraId, boolean enabled)108   protected void setTorchMode(@Nonnull String cameraId, boolean enabled) {
109     Preconditions.checkNotNull(cameraId);
110     Preconditions.checkArgument(cameraIdToCharacteristics.keySet().contains(cameraId));
111     cameraTorches.put(cameraId, enabled);
112     for (Object callback : torchCallbacks) {
113       ((CameraManager.TorchCallback) callback).onTorchModeChanged(cameraId, enabled);
114     }
115   }
116 
117   @Implementation(minSdk = U.SDK_INT, maxSdk = U.SDK_INT)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid, final int oomScoreOffset, boolean overrideToPortrait)118   protected CameraDevice openCameraDeviceUserAsync(
119       String cameraId,
120       CameraDevice.StateCallback callback,
121       Executor executor,
122       final int uid,
123       final int oomScoreOffset,
124       boolean overrideToPortrait) {
125     return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset);
126   }
127 
128   @Implementation(minSdk = V.SDK_INT, maxSdk = V.SDK_INT)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid, final int oomScoreOffset, int rotationOverride)129   protected CameraDevice openCameraDeviceUserAsync(
130       String cameraId,
131       CameraDevice.StateCallback callback,
132       Executor executor,
133       final int uid,
134       final int oomScoreOffset,
135       int rotationOverride) {
136     return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset);
137   }
138 
139   // in development API has reverted back to the T signature. Just use a different method name
140   // to avoid conflicts.
141   // TODO: increment this to  minSdk next-SDK-after-V once V is fully released
142   @Implementation(methodName = "openCameraDeviceUserAsync", minSdk = Baklava.SDK_INT)
143   @InDevelopment
openCameraDeviceUserAsyncPostV( String cameraId, CameraDevice.StateCallback callback, Executor executor, int unusedClientUid, int unusedOomScoreOffset, boolean unused)144   protected CameraDevice openCameraDeviceUserAsyncPostV(
145       String cameraId,
146       CameraDevice.StateCallback callback,
147       Executor executor,
148       int unusedClientUid,
149       int unusedOomScoreOffset,
150       boolean unused) {
151     return openCameraDeviceUserAsync(
152         cameraId, callback, executor, unusedClientUid, unusedOomScoreOffset);
153   }
154 
155   @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, int unusedClientUid, int unusedOomScoreOffset)156   protected CameraDevice openCameraDeviceUserAsync(
157       String cameraId,
158       CameraDevice.StateCallback callback,
159       Executor executor,
160       int unusedClientUid,
161       int unusedOomScoreOffset) {
162     CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
163     Context context = RuntimeEnvironment.getApplication();
164     CameraDeviceImpl deviceImpl =
165         createCameraDeviceImpl(cameraId, callback, executor, characteristics, context);
166     createdCameras.add(deviceImpl);
167     updateCameraCallback(deviceImpl, callback, null, executor);
168     executor.execute(() -> callback.onOpened(deviceImpl));
169     return deviceImpl;
170   }
171 
172   @Implementation(minSdk = VERSION_CODES.P, maxSdk = VERSION_CODES.R)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid)173   protected CameraDevice openCameraDeviceUserAsync(
174       String cameraId, CameraDevice.StateCallback callback, Executor executor, final int uid)
175       throws CameraAccessException {
176     CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
177     Context context = reflector(ReflectorCameraManager.class, realObject).getContext();
178 
179     CameraDeviceImpl deviceImpl =
180         ReflectionHelpers.callConstructor(
181             CameraDeviceImpl.class,
182             ClassParameter.from(String.class, cameraId),
183             ClassParameter.from(CameraDevice.StateCallback.class, callback),
184             ClassParameter.from(Executor.class, executor),
185             ClassParameter.from(CameraCharacteristics.class, characteristics),
186             ClassParameter.from(int.class, context.getApplicationInfo().targetSdkVersion));
187 
188     createdCameras.add(deviceImpl);
189     updateCameraCallback(deviceImpl, callback, null, executor);
190     executor.execute(() -> callback.onOpened(deviceImpl));
191     return deviceImpl;
192   }
193 
194   @Implementation(minSdk = VERSION_CODES.N_MR1, maxSdk = VERSION_CODES.O_MR1)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Handler handler, final int uid)195   protected CameraDevice openCameraDeviceUserAsync(
196       String cameraId, CameraDevice.StateCallback callback, Handler handler, final int uid)
197       throws CameraAccessException {
198     CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
199     Context context = reflector(ReflectorCameraManager.class, realObject).getContext();
200 
201     CameraDeviceImpl deviceImpl;
202     if (Build.VERSION.SDK_INT == VERSION_CODES.N_MR1) {
203       deviceImpl =
204           ReflectionHelpers.callConstructor(
205               CameraDeviceImpl.class,
206               ClassParameter.from(String.class, cameraId),
207               ClassParameter.from(CameraDevice.StateCallback.class, callback),
208               ClassParameter.from(Handler.class, handler),
209               ClassParameter.from(CameraCharacteristics.class, characteristics));
210     } else {
211       deviceImpl =
212           ReflectionHelpers.callConstructor(
213               CameraDeviceImpl.class,
214               ClassParameter.from(String.class, cameraId),
215               ClassParameter.from(CameraDevice.StateCallback.class, callback),
216               ClassParameter.from(Handler.class, handler),
217               ClassParameter.from(CameraCharacteristics.class, characteristics),
218               ClassParameter.from(int.class, context.getApplicationInfo().targetSdkVersion));
219     }
220     createdCameras.add(deviceImpl);
221     updateCameraCallback(deviceImpl, callback, handler, null);
222     handler.post(() -> callback.onOpened(deviceImpl));
223     return deviceImpl;
224   }
225 
226   /**
227    * Enables {@link CameraManager#openCamera(String, StateCallback, Handler)} to open a {@link
228    * CameraDevice}.
229    *
230    * <p>If the provided cameraId exists, this will always post {@link
231    * CameraDevice.StateCallback#onOpened(CameraDevice)} to the provided {@link Handler}. Unlike on
232    * real Android, this will not check if the camera has been disabled by device policy and does not
233    * attempt to connect to the camera service, so {@link
234    * CameraDevice.StateCallback#onError(CameraDevice, int)} and {@link
235    * CameraDevice.StateCallback#onDisconnected(CameraDevice)} will not be triggered by {@link
236    * CameraManager#openCamera(String, StateCallback, Handler)}.
237    */
238   @Implementation(maxSdk = VERSION_CODES.N)
openCameraDeviceUserAsync( String cameraId, CameraDevice.StateCallback callback, Handler handler)239   protected CameraDevice openCameraDeviceUserAsync(
240       String cameraId, CameraDevice.StateCallback callback, Handler handler)
241       throws CameraAccessException {
242     CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
243 
244     CameraDeviceImpl deviceImpl =
245         ReflectionHelpers.callConstructor(
246             CameraDeviceImpl.class,
247             ClassParameter.from(String.class, cameraId),
248             ClassParameter.from(CameraDevice.StateCallback.class, callback),
249             ClassParameter.from(Handler.class, handler),
250             ClassParameter.from(CameraCharacteristics.class, characteristics));
251 
252     createdCameras.add(deviceImpl);
253     updateCameraCallback(deviceImpl, callback, handler, null);
254     handler.post(() -> callback.onOpened(deviceImpl));
255     return deviceImpl;
256   }
257 
258   @Implementation
registerAvailabilityCallback( CameraManager.AvailabilityCallback callback, Handler handler)259   protected void registerAvailabilityCallback(
260       CameraManager.AvailabilityCallback callback, Handler handler) {
261     Preconditions.checkNotNull(callback);
262     registeredCallbacks.add(callback);
263   }
264 
265   @Implementation
unregisterAvailabilityCallback(CameraManager.AvailabilityCallback callback)266   protected void unregisterAvailabilityCallback(CameraManager.AvailabilityCallback callback) {
267     Preconditions.checkNotNull(callback);
268     registeredCallbacks.remove(callback);
269   }
270 
271   @Implementation(minSdk = VERSION_CODES.M)
registerTorchCallback(CameraManager.TorchCallback callback, Handler handler)272   protected void registerTorchCallback(CameraManager.TorchCallback callback, Handler handler) {
273     Preconditions.checkNotNull(callback);
274     torchCallbacks.add(callback);
275   }
276 
277   @Implementation(minSdk = VERSION_CODES.M)
unregisterTorchCallback(CameraManager.TorchCallback callback)278   protected void unregisterTorchCallback(CameraManager.TorchCallback callback) {
279     Preconditions.checkNotNull(callback);
280     torchCallbacks.remove(callback);
281   }
282 
createCameraDeviceImpl( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, Context context)283   private CameraDeviceImpl createCameraDeviceImpl(
284       String cameraId,
285       CameraDevice.StateCallback callback,
286       Executor executor,
287       CameraCharacteristics characteristics,
288       Context context) {
289     Map<String, CameraCharacteristics> cameraCharacteristicsMap = Collections.emptyMap();
290     if (RuntimeEnvironment.getApiLevel() >= Baklava.SDK_INT) {
291       return reflector(ReflectorCameraDeviceImpl.class)
292           .newCameraDeviceImplPostV(
293               cameraId,
294               callback,
295               executor,
296               characteristics,
297               realObject,
298               context.getApplicationInfo().targetSdkVersion,
299               context,
300               null,
301               false);
302 
303     } else if (RuntimeEnvironment.getApiLevel() == V.SDK_INT) {
304       return reflector(ReflectorCameraDeviceImpl.class)
305           .newCameraDeviceImplV(
306               cameraId,
307               callback,
308               executor,
309               characteristics,
310               realObject,
311               context.getApplicationInfo().targetSdkVersion,
312               context,
313               null);
314     } else {
315       return reflector(ReflectorCameraDeviceImpl.class)
316           .newCameraDeviceImpl(
317               cameraId,
318               callback,
319               executor,
320               characteristics,
321               cameraCharacteristicsMap,
322               context.getApplicationInfo().targetSdkVersion,
323               context);
324     }
325   }
326 
327   /**
328    * Calls all registered callbacks's onCameraAvailable method. This is a no-op if no callbacks are
329    * registered.
330    */
triggerOnCameraAvailable(@onnull String cameraId)331   private void triggerOnCameraAvailable(@Nonnull String cameraId) {
332     Preconditions.checkNotNull(cameraId);
333     for (CameraManager.AvailabilityCallback callback : registeredCallbacks) {
334       callback.onCameraAvailable(cameraId);
335     }
336   }
337 
338   /**
339    * Calls all registered callbacks's onCameraUnavailable method. This is a no-op if no callbacks
340    * are registered.
341    */
triggerOnCameraUnavailable(@onnull String cameraId)342   private void triggerOnCameraUnavailable(@Nonnull String cameraId) {
343     Preconditions.checkNotNull(cameraId);
344     for (CameraManager.AvailabilityCallback callback : registeredCallbacks) {
345       callback.onCameraUnavailable(cameraId);
346     }
347   }
348 
349   /**
350    * Adds the given cameraId and characteristics to this shadow.
351    *
352    * <p>The result from {@link #getCameraIdList()} will be in the order in which cameras were added.
353    *
354    * @throws IllegalArgumentException if there's already an existing camera with the given id.
355    */
addCamera(@onnull String cameraId, @Nonnull CameraCharacteristics characteristics)356   public void addCamera(@Nonnull String cameraId, @Nonnull CameraCharacteristics characteristics) {
357     Preconditions.checkNotNull(cameraId);
358     Preconditions.checkNotNull(characteristics);
359     Preconditions.checkArgument(!cameraIdToCharacteristics.containsKey(cameraId));
360 
361     cameraIdToCharacteristics.put(cameraId, characteristics);
362     triggerOnCameraAvailable(cameraId);
363   }
364 
365   /**
366    * Removes the given cameraId and associated characteristics from this shadow.
367    *
368    * @throws IllegalArgumentException if there is not an existing camera with the given id.
369    */
removeCamera(@onnull String cameraId)370   public void removeCamera(@Nonnull String cameraId) {
371     Preconditions.checkNotNull(cameraId);
372     Preconditions.checkArgument(cameraIdToCharacteristics.containsKey(cameraId));
373 
374     cameraIdToCharacteristics.remove(cameraId);
375     triggerOnCameraUnavailable(cameraId);
376   }
377 
378   /** Returns what the supplied camera's torch is set to. */
getTorchMode(@onnull String cameraId)379   public boolean getTorchMode(@Nonnull String cameraId) {
380     Preconditions.checkNotNull(cameraId);
381     Preconditions.checkArgument(cameraIdToCharacteristics.keySet().contains(cameraId));
382     Boolean torchState = cameraTorches.get(cameraId);
383     return torchState;
384   }
385 
386   /**
387    * Triggers a disconnect event, where any open camera will be disconnected (simulating the case
388    * where another app takes control of the camera).
389    */
triggerDisconnect()390   public void triggerDisconnect() {
391     if (lastCallbackHandler != null) {
392       lastCallbackHandler.post(() -> lastCallback.onDisconnected(lastDevice));
393     } else if (lastCallbackExecutor != null) {
394       lastCallbackExecutor.execute(() -> lastCallback.onDisconnected(lastDevice));
395     }
396   }
397 
updateCameraCallback( CameraDevice device, CameraDevice.StateCallback callback, @Nullable Handler handler, @Nullable Executor executor)398   protected void updateCameraCallback(
399       CameraDevice device,
400       CameraDevice.StateCallback callback,
401       @Nullable Handler handler,
402       @Nullable Executor executor) {
403     lastDevice = device;
404     lastCallback = callback;
405     lastCallbackHandler = handler;
406     lastCallbackExecutor = executor;
407   }
408 
409   @ForType(CameraDeviceImpl.class)
410   interface ReflectorCameraDeviceImpl {
411     @Constructor
newCameraDeviceImpl( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, Map<String, CameraCharacteristics> characteristicsMap, int targetSdkVersion, Context context)412     CameraDeviceImpl newCameraDeviceImpl(
413         String cameraId,
414         CameraDevice.StateCallback callback,
415         Executor executor,
416         CameraCharacteristics characteristics,
417         Map<String, CameraCharacteristics> characteristicsMap,
418         int targetSdkVersion,
419         Context context);
420 
421     @Constructor
newCameraDeviceImplV( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, CameraManager cameraManager, int targetSdkVersion, Context context, @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup)422     CameraDeviceImpl newCameraDeviceImplV(
423         String cameraId,
424         CameraDevice.StateCallback callback,
425         Executor executor,
426         CameraCharacteristics characteristics,
427         CameraManager cameraManager,
428         int targetSdkVersion,
429         Context context,
430         @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup")
431             Object cameraDeviceSetup);
432 
433     @Constructor
newCameraDeviceImplPostV( String cameraId, CameraDevice.StateCallback callback, Executor executor, CameraCharacteristics characteristics, CameraManager cameraManager, int targetSdkVersion, Context context, @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup") Object cameraDeviceSetup, boolean unused)434     CameraDeviceImpl newCameraDeviceImplPostV(
435         String cameraId,
436         CameraDevice.StateCallback callback,
437         Executor executor,
438         CameraCharacteristics characteristics,
439         CameraManager cameraManager,
440         int targetSdkVersion,
441         Context context,
442         @WithType("android.hardware.camera2.CameraDevice$CameraDeviceSetup")
443             Object cameraDeviceSetup,
444         boolean unused);
445   }
446 
447   /** Accessor interface for {@link CameraManager}'s internals. */
448   @ForType(CameraManager.class)
449   private interface ReflectorCameraManager {
450 
451     @Accessor("mContext")
getContext()452     Context getContext();
453   }
454 
455   /** Shadow class for internal class CameraManager$CameraManagerGlobal */
456   @Implements(
457       className = "android.hardware.camera2.CameraManager$CameraManagerGlobal",
458       minSdk = VERSION_CODES.LOLLIPOP_MR1)
459   public static class ShadowCameraManagerGlobal {
460 
461     /**
462      * Cannot create a CameraService connection within Robolectric. Avoid endless reconnect loop.
463      */
464     @Implementation(minSdk = VERSION_CODES.N)
scheduleCameraServiceReconnectionLocked()465     protected void scheduleCameraServiceReconnectionLocked() {}
466   }
467 }
468