1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.testingcamera2;
18 
19 import java.util.ArrayList;
20 import java.util.HashSet;
21 import java.util.LinkedList;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Set;
25 
26 import android.content.Context;
27 import android.util.AttributeSet;
28 import android.view.LayoutInflater;
29 import android.view.Surface;
30 import android.view.View;
31 import android.widget.AdapterView;
32 import android.widget.AdapterView.OnItemSelectedListener;
33 import android.widget.ArrayAdapter;
34 import android.widget.Button;
35 import android.widget.CompoundButton;
36 import android.widget.Spinner;
37 import android.widget.TextView;
38 import android.widget.ToggleButton;
39 import android.hardware.camera2.CameraAccessException;
40 import android.hardware.camera2.CameraCaptureSession;
41 import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
42 import android.hardware.camera2.CameraCharacteristics;
43 import android.hardware.camera2.CameraDevice;
44 import android.hardware.camera2.CameraManager;
45 import android.hardware.camera2.CaptureRequest;
46 import android.hardware.camera2.CaptureResult;
47 import android.hardware.camera2.TotalCaptureResult;
48 
49 import org.xmlpull.v1.XmlPullParser;
50 import org.xmlpull.v1.XmlPullParserException;
51 
52 import com.android.testingcamera2.PaneTracker.PaneEvent;
53 
54 import java.io.IOException;
55 
56 /**
57  *
58  * Basic control pane block for the control list
59  *
60  */
61 public class CameraControlPane extends ControlPane {
62 
63     // XML attributes
64 
65     /** Name of pane tag */
66     private static final String PANE_NAME = "camera_pane";
67 
68     /** Attribute: ID for pane (integer) */
69     private static final String PANE_ID = "id";
70     /** Attribute: ID for camera to select (String) */
71     private static final String CAMERA_ID = "camera_id";
72 
73     // End XML attributes
74 
75     private static final int MAX_CACHED_RESULTS = 100;
76 
77     private static int mCameraPaneIdCounter = 0;
78 
79     /**
80      * These correspond to the callbacks from
81      * android.hardware.camera2.CameraDevice.StateCallback, plus UNAVAILABLE for
82      * when there's not a valid camera selected.
83      */
84     private enum CameraState {
85         UNAVAILABLE,
86         CLOSED,
87         OPENED,
88         DISCONNECTED,
89         ERROR
90     }
91 
92     /**
93      * These correspond to the callbacks from {@link CameraCaptureSession.StateCallback}, plus
94      * {@code CONFIGURING} for before a session is returned and {@code NONE} for when there
95      * is no session created.
96      */
97     private enum SessionState {
98         NONE,
99         CONFIGURED,
100         CONFIGURE_FAILED,
101         READY,
102         ACTIVE,
103         CLOSED
104     }
105 
106     private enum CameraCall {
107         NONE,
108         CONFIGURE
109     }
110 
111     private final int mPaneId;
112 
113     private CameraOps2 mCameraOps;
114     private InfoDisplayer mInfoDisplayer;
115 
116     private Spinner mCameraSpinner;
117     private ToggleButton mOpenButton;
118     private Button mInfoButton;
119     private TextView mStatusText;
120     private Button mConfigureButton;
121     private Button mStopButton;
122     private Button mFlushButton;
123 
124     /**
125      * All controls that should be enabled when there's a valid camera ID
126      * selected
127      */
128     private final Set<View> mBaseControls = new HashSet<View>();
129     /**
130      * All controls that should be enabled when camera is at least in the OPEN
131      * state
132      */
133     private final Set<View> mOpenControls = new HashSet<View>();
134     /**
135      * All controls that should be enabled when camera is at least in the IDLE
136      * state
137      */
138     private final Set<View> mConfiguredControls = new HashSet<View>();
139 
140     private String[] mCameraIds;
141     private String mCurrentCameraId;
142 
143     private CameraState mCameraState;
144     private CameraDevice mCurrentCamera;
145     private CameraCaptureSession mCurrentCaptureSession;
146     private SessionState mSessionState = SessionState.NONE;
147     private CameraCall mActiveCameraCall;
148     private LinkedList<TotalCaptureResult> mRecentResults = new LinkedList<>();
149 
150     private List<Surface> mConfiguredSurfaces;
151     private List<TargetControlPane> mConfiguredTargetPanes;
152 
153     /**
154      * Constructor for tooling only
155      */
CameraControlPane(Context context, AttributeSet attrs)156     public CameraControlPane(Context context, AttributeSet attrs) {
157         super(context, attrs, null, null);
158 
159         mPaneId = 0;
160         setUpUI(context);
161     }
162 
CameraControlPane(TestingCamera21 tc, AttributeSet attrs, StatusListener listener)163     public CameraControlPane(TestingCamera21 tc, AttributeSet attrs, StatusListener listener) {
164 
165         super(tc, attrs, listener, tc.getPaneTracker());
166 
167         mPaneId = mCameraPaneIdCounter++;
168         setUpUI(tc);
169         initializeCameras(tc);
170 
171         if (mCameraIds != null) {
172             switchToCamera(mCameraIds[0]);
173         }
174     }
175 
CameraControlPane(TestingCamera21 tc, XmlPullParser configParser, StatusListener listener)176     public CameraControlPane(TestingCamera21 tc, XmlPullParser configParser, StatusListener listener)
177             throws XmlPullParserException, IOException {
178         super(tc, null, listener, tc.getPaneTracker());
179 
180         configParser.require(XmlPullParser.START_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);
181 
182         int paneId = getAttributeInt(configParser, PANE_ID, -1);
183         if (paneId == -1) {
184             mPaneId = mCameraPaneIdCounter++;
185         } else {
186             mPaneId = paneId;
187             if (mPaneId >= mCameraPaneIdCounter) {
188                 mCameraPaneIdCounter = mPaneId + 1;
189             }
190         }
191 
192         String cameraId = getAttributeString(configParser, CAMERA_ID, null);
193 
194         configParser.next();
195         configParser.require(XmlPullParser.END_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);
196 
197         setUpUI(tc);
198         initializeCameras(tc);
199 
200         boolean gotCamera = false;
201         if (mCameraIds != null && cameraId != null) {
202             for (int i = 0; i < mCameraIds.length; i++) {
203                 if (cameraId.equals(mCameraIds[i])) {
204                     switchToCamera(mCameraIds[i]);
205                     mCameraSpinner.setSelection(i);
206                     gotCamera = true;
207                 }
208             }
209         }
210 
211         if (!gotCamera && mCameraIds != null) {
212             switchToCamera(mCameraIds[0]);
213         }
214     }
215 
216     @Override
remove()217     public void remove() {
218         closeCurrentCamera();
219         super.remove();
220     }
221 
222     /**
223      * Get list of target panes that are currently actively configured for this
224      * camera
225      */
getCurrentConfiguredTargets()226     public List<TargetControlPane> getCurrentConfiguredTargets() {
227         return mConfiguredTargetPanes;
228     }
229 
230     /**
231      * Interface to be implemented by an application service for displaying a
232      * camera's information.
233      */
234     public interface InfoDisplayer {
showCameraInfo(String cameraId)235         public void showCameraInfo(String cameraId);
236     }
237 
getCharacteristics()238     public CameraCharacteristics getCharacteristics() {
239         if (mCurrentCameraId != null) {
240             return mCameraOps.getCameraInfo(mCurrentCameraId);
241         }
242         return null;
243     }
244 
getRequestBuilder(int template)245     public CaptureRequest.Builder getRequestBuilder(int template) {
246         CaptureRequest.Builder request = null;
247         if (mCurrentCamera != null) {
248             try {
249                 request = mCurrentCamera.createCaptureRequest(template);
250                 // Workaround for b/15748139
251                 request.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
252                         CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON);
253             } catch (CameraAccessException e) {
254                 TLog.e("Unable to build request for camera %s with template %d.", e,
255                         mCurrentCameraId, template);
256             }
257         }
258         return request;
259     }
260 
261     /**
262      * Send single capture to camera device.
263      *
264      * @param request
265      * @return true if capture sent successfully
266      */
capture(CaptureRequest request)267     public boolean capture(CaptureRequest request) {
268         if (mCurrentCaptureSession != null) {
269             try {
270                 mCurrentCaptureSession.capture(request, mResultListener, null);
271                 return true;
272             } catch (CameraAccessException e) {
273                 TLog.e("Unable to capture for camera %s.", e, mCurrentCameraId);
274             }
275         }
276         return false;
277     }
278 
repeat(CaptureRequest request)279     public boolean repeat(CaptureRequest request) {
280         if (mCurrentCaptureSession != null) {
281             try {
282                 mCurrentCaptureSession.setRepeatingRequest(request, mResultListener, null);
283                 return true;
284             } catch (CameraAccessException e) {
285                 TLog.e("Unable to set repeating request for camera %s.", e, mCurrentCameraId);
286             }
287         }
288         return false;
289     }
290 
getResultAt(long timestamp)291     public TotalCaptureResult getResultAt(long timestamp) {
292         for (TotalCaptureResult result : mRecentResults) {
293             long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
294             if (resultTimestamp == timestamp) return result;
295             if (resultTimestamp > timestamp) return null;
296         }
297         return null;
298     }
299 
prepareSurface(Surface target)300     public void prepareSurface(Surface target) {
301         if (mCurrentCaptureSession != null) {
302             try {
303                 TLog.i("Preparing Surface " + target);
304                 mCurrentCaptureSession.prepare(target);
305             } catch (CameraAccessException e) {
306                 TLog.e("Unable to prepare surface for camera %s.", e, mCurrentCameraId);
307             } catch (IllegalArgumentException e) {
308                 TLog.e("Bad Surface passed to prepare", e);
309             }
310         }
311     }
312 
313     private CaptureCallback mResultListener = new CaptureCallback() {
314         public void onCaptureCompleted(
315                 CameraCaptureSession session,
316                 CaptureRequest request,
317                 TotalCaptureResult result) {
318             mRecentResults.add(result);
319             if (mRecentResults.size() > MAX_CACHED_RESULTS) {
320                 mRecentResults.remove();
321             }
322         }
323     };
324 
setUpUI(Context context)325     private void setUpUI(Context context) {
326         String paneName =
327                 String.format(Locale.US, "%s %c",
328                         context.getResources().getString(R.string.camera_pane_title),
329                         (char) ('A' + mPaneId));
330         this.setName(paneName);
331 
332         LayoutInflater inflater =
333                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
334 
335         inflater.inflate(R.layout.camera_pane, this);
336 
337         mCameraSpinner = (Spinner) findViewById(R.id.camera_pane_camera_spinner);
338         mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
339 
340         mOpenButton = (ToggleButton) findViewById(R.id.camera_pane_open_button);
341         mOpenButton.setOnCheckedChangeListener(mOpenButtonListener);
342         mBaseControls.add(mOpenButton);
343 
344         mInfoButton = (Button) findViewById(R.id.camera_pane_info_button);
345         mInfoButton.setOnClickListener(mInfoButtonListener);
346         mBaseControls.add(mInfoButton);
347 
348         mStatusText = (TextView) findViewById(R.id.camera_pane_status_text);
349 
350         mConfigureButton = (Button) findViewById(R.id.camera_pane_configure_button);
351         mConfigureButton.setOnClickListener(mConfigureButtonListener);
352         mOpenControls.add(mConfigureButton);
353 
354         mStopButton = (Button) findViewById(R.id.camera_pane_stop_button);
355         mStopButton.setOnClickListener(mStopButtonListener);
356         mConfiguredControls.add(mStopButton);
357         mFlushButton = (Button) findViewById(R.id.camera_pane_flush_button);
358         mFlushButton.setOnClickListener(mFlushButtonListener);
359         mConfiguredControls.add(mFlushButton);
360     }
361 
initializeCameras(TestingCamera21 tc)362     private void initializeCameras(TestingCamera21 tc) {
363         mCameraOps = tc.getCameraOps();
364         mInfoDisplayer = tc;
365 
366         updateCameraList();
367     }
368 
updateCameraList()369     private void updateCameraList() {
370         mCameraIds = null;
371         try {
372             mCameraIds = mCameraOps.getCamerasAndListen(mCameraAvailabilityCallback);
373             String[] cameraSpinnerItems = new String[mCameraIds.length];
374             for (int i = 0; i < mCameraIds.length; i++) {
375                 cameraSpinnerItems[i] = String.format("Camera %s", mCameraIds[i]);
376             }
377             mCameraSpinner.setAdapter(new ArrayAdapter<String>(getContext(), R.layout.spinner_item,
378                     cameraSpinnerItems));
379 
380         } catch (CameraAccessException e) {
381             TLog.e("Exception trying to get list of cameras: " + e);
382         }
383     }
384 
385     private final CompoundButton.OnCheckedChangeListener mOpenButtonListener =
386             new CompoundButton.OnCheckedChangeListener() {
387                 @Override
388                 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
389                     if (isChecked) {
390                         // Open camera
391                         mCurrentCamera = null;
392                         mCameraOps.openCamera(mCurrentCameraId, mCameraListener);
393                     } else {
394                         // Close camera
395                         closeCurrentCamera();
396                     }
397                 }
398             };
399 
400     private final OnClickListener mInfoButtonListener = new OnClickListener() {
401         @Override
402         public void onClick(View v) {
403             mInfoDisplayer.showCameraInfo(mCurrentCameraId);
404         }
405     };
406 
407     private final OnClickListener mStopButtonListener = new OnClickListener() {
408         @Override
409         public void onClick(View v) {
410             if (mCurrentCaptureSession != null) {
411                 try {
412                     mCurrentCaptureSession.stopRepeating();
413                 } catch (CameraAccessException e) {
414                     TLog.e("Unable to stop repeating request for camera %s.", e, mCurrentCameraId);
415                 }
416             }
417         }
418     };
419 
420     private final OnClickListener mFlushButtonListener = new OnClickListener() {
421         @Override
422         public void onClick(View v) {
423             if (mCurrentCaptureSession != null) {
424                 try {
425                     mCurrentCaptureSession.abortCaptures();
426                 } catch (CameraAccessException e) {
427                     TLog.e("Unable to flush camera %s.", e, mCurrentCameraId);
428                 }
429             }
430         }
431     };
432 
433     private final OnClickListener mConfigureButtonListener = new OnClickListener() {
434         @Override
435         public void onClick(View v) {
436             List<Surface> targetSurfaces = new ArrayList<Surface>();
437             List<TargetControlPane> targetPanes = new ArrayList<TargetControlPane>();
438             for (TargetControlPane targetPane : mPaneTracker.getPanes(TargetControlPane.class)) {
439                 Surface target = targetPane.getTargetSurfaceForCameraPane(getPaneName());
440                 if (target != null) {
441                     targetSurfaces.add(target);
442                     targetPanes.add(targetPane);
443                 }
444             }
445             try {
446                 TLog.i("Configuring camera %s with %d surfaces", mCurrentCamera.getId(),
447                         targetSurfaces.size());
448                 mActiveCameraCall = CameraCall.CONFIGURE;
449                 if (targetSurfaces.size() > 0) {
450                     mCurrentCamera.createCaptureSession(targetSurfaces, mSessionListener, /*handler*/null);
451                 } else if (mCurrentCaptureSession != null) {
452                     mCurrentCaptureSession.close();
453                     mCurrentCaptureSession = null;
454                 }
455                 mConfiguredSurfaces = targetSurfaces;
456                 mConfiguredTargetPanes = targetPanes;
457             } catch (CameraAccessException e) {
458                 mActiveCameraCall = CameraCall.NONE;
459                 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
460             } catch (IllegalArgumentException e) {
461                 mActiveCameraCall = CameraCall.NONE;
462                 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
463             } catch (IllegalStateException e) {
464                 mActiveCameraCall = CameraCall.NONE;
465                 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
466             }
467         }
468     };
469 
470     private final CameraCaptureSession.StateCallback mSessionListener =
471             new CameraCaptureSession.StateCallback() {
472 
473         @Override
474         public void onConfigured(CameraCaptureSession session) {
475             mCurrentCaptureSession = session;
476             TLog.i("Configuration completed for camera %s.", mCurrentCamera.getId());
477 
478             setSessionState(SessionState.CONFIGURED);
479         }
480 
481         @Override
482         public void onConfigureFailed(CameraCaptureSession session) {
483             mActiveCameraCall = CameraCall.NONE;
484             TLog.e("Configuration failed for camera %s.", mCurrentCamera.getId());
485 
486             setSessionState(SessionState.CONFIGURE_FAILED);
487         }
488 
489         @Override
490         public void onReady(CameraCaptureSession session) {
491             setSessionState(SessionState.READY);
492         }
493 
494         /**
495          * This method is called when the session starts actively processing capture requests.
496          *
497          * <p>If capture requests are submitted prior to {@link #onConfigured} being called,
498          * then the session will start processing those requests immediately after the callback,
499          * and this method will be immediately called after {@link #onConfigured}.
500          *
501          * <p>If the session runs out of capture requests to process and calls {@link #onReady},
502          * then this callback will be invoked again once new requests are submitted for capture.</p>
503          */
504         @Override
505         public void onActive(CameraCaptureSession session) {
506             setSessionState(SessionState.ACTIVE);
507         }
508 
509         /**
510          * This method is called when the session is closed.
511          *
512          * <p>A session is closed when a new session is created by the parent camera device,
513          * or when the parent camera device is closed (either by the user closing the device,
514          * or due to a camera device disconnection or fatal error).</p>
515          *
516          * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and
517          * any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called).
518          * However, any in-progress capture requests submitted to the session will be completed
519          * as normal.</p>
520          */
521         @Override
522         public void onClosed(CameraCaptureSession session) {
523             // Ignore closes if the session has been replaced
524             if (mCurrentCaptureSession != null && session != mCurrentCaptureSession) {
525                 return;
526             }
527             setSessionState(SessionState.CLOSED);
528         }
529 
530         @Override
531         public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
532             TLog.i("Surface preparation complete for Surface " + surface);
533         }
534 
535     };
536 
537     private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() {
538         @Override
539         public void onClosed(CameraDevice camera) {
540             // Don't change state on close, tracked by callers of close()
541             mOpenButton.setChecked(false);
542         }
543 
544         @Override
545         public void onDisconnected(CameraDevice camera) {
546             setCameraState(CameraState.DISCONNECTED);
547         }
548 
549         @Override
550         public void onError(CameraDevice camera, int error) {
551             setCameraState(CameraState.ERROR);
552         }
553 
554         @Override
555         public void onOpened(CameraDevice camera) {
556             mCurrentCamera = camera;
557             setCameraState(CameraState.OPENED);
558         }
559     };
560 
switchToCamera(String newCameraId)561     private void switchToCamera(String newCameraId) {
562         closeCurrentCamera();
563 
564         mCurrentCameraId = newCameraId;
565 
566         if (mCurrentCameraId == null) {
567             setCameraState(CameraState.UNAVAILABLE);
568         } else {
569             setCameraState(CameraState.CLOSED);
570         }
571 
572         mPaneTracker.notifyOtherPanes(this, PaneTracker.PaneEvent.NEW_CAMERA_SELECTED);
573     }
574 
closeCurrentCamera()575     private void closeCurrentCamera() {
576         if (mCurrentCamera != null) {
577             mCurrentCamera.close();
578             mCurrentCamera = null;
579             setCameraState(CameraState.CLOSED);
580         }
581     }
582 
setSessionState(SessionState newState)583     private void setSessionState(SessionState newState) {
584         mSessionState = newState;
585         mStatusText.setText("S." + mSessionState.toString());
586 
587         switch (mSessionState) {
588             case CONFIGURE_FAILED:
589                 mActiveCameraCall = CameraCall.NONE;
590                 // fall-through
591             case CLOSED:
592                 enableBaseControls(true);
593                 enableOpenControls(true);
594                 enableConfiguredControls(false);
595                 mConfiguredTargetPanes = null;
596                 break;
597             case NONE:
598                 enableBaseControls(true);
599                 enableOpenControls(true);
600                 enableConfiguredControls(false);
601                 mConfiguredTargetPanes = null;
602                 break;
603             case CONFIGURED:
604                 if (mActiveCameraCall != CameraCall.CONFIGURE) {
605                     throw new AssertionError();
606                 }
607                 mPaneTracker.notifyOtherPanes(this, PaneEvent.CAMERA_CONFIGURED);
608                 mActiveCameraCall = CameraCall.NONE;
609                 // fall-through
610             case READY:
611             case ACTIVE:
612                 enableBaseControls(true);
613                 enableOpenControls(true);
614                 enableConfiguredControls(true);
615                 break;
616             default:
617                 throw new AssertionError("Unhandled case " + mSessionState);
618         }
619     }
620 
setCameraState(CameraState newState)621     private void setCameraState(CameraState newState) {
622         mCameraState = newState;
623         mStatusText.setText("C." + mCameraState.toString());
624         switch (mCameraState) {
625             case UNAVAILABLE:
626                 enableBaseControls(false);
627                 enableOpenControls(false);
628                 enableConfiguredControls(false);
629                 mConfiguredTargetPanes = null;
630                 break;
631             case CLOSED:
632             case DISCONNECTED:
633             case ERROR:
634                 enableBaseControls(true);
635                 enableOpenControls(false);
636                 enableConfiguredControls(false);
637                 mConfiguredTargetPanes = null;
638                 break;
639             case OPENED:
640                 enableBaseControls(true);
641                 enableOpenControls(true);
642                 enableConfiguredControls(false);
643                 mConfiguredTargetPanes = null;
644                 break;
645         }
646     }
647 
enableBaseControls(boolean enabled)648     private void enableBaseControls(boolean enabled) {
649         for (View v : mBaseControls) {
650             v.setEnabled(enabled);
651         }
652         if (!enabled) {
653             mOpenButton.setChecked(false);
654         }
655     }
656 
enableOpenControls(boolean enabled)657     private void enableOpenControls(boolean enabled) {
658         for (View v : mOpenControls) {
659             v.setEnabled(enabled);
660         }
661     }
662 
enableConfiguredControls(boolean enabled)663     private void enableConfiguredControls(boolean enabled) {
664         for (View v : mConfiguredControls) {
665             v.setEnabled(enabled);
666         }
667     }
668 
669     private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback =
670             new CameraManager.AvailabilityCallback() {
671         @Override
672         public void onCameraAvailable(String cameraId) {
673             // TODO: Update camera list in an intelligent fashion
674             // (can't just call updateCameraList or the selected camera may change)
675         }
676 
677         @Override
678         public void onCameraUnavailable(String cameraId) {
679             // TODO: Update camera list in an intelligent fashion
680             // (can't just call updateCameraList or the selected camera may change)
681         }
682     };
683 
684     private final OnItemSelectedListener mCameraSpinnerListener = new OnItemSelectedListener() {
685         @Override
686         public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
687             String newCameraId = mCameraIds[pos];
688             if (newCameraId != mCurrentCameraId) {
689                 switchToCamera(newCameraId);
690             }
691         }
692 
693         @Override
694         public void onNothingSelected(AdapterView<?> parent) {
695             switchToCamera(null);
696         }
697     };
698 }
699