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