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