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.CaptureCallback;
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.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     */
156    public CameraControlPane(Context context, AttributeSet attrs) {
157        super(context, attrs, null, null);
158
159        mPaneId = 0;
160        setUpUI(context);
161    }
162
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
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
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     */
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 {
235        public void showCameraInfo(String cameraId);
236    }
237
238    public CameraCharacteristics getCharacteristics() {
239        if (mCurrentCameraId != null) {
240            return mCameraOps.getCameraInfo(mCurrentCameraId);
241        }
242        return null;
243    }
244
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     */
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
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
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
300    private CaptureCallback mResultListener = new CaptureCallback() {
301        public void onCaptureCompleted(
302                CameraCaptureSession session,
303                CaptureRequest request,
304                TotalCaptureResult result) {
305            mRecentResults.add(result);
306            if (mRecentResults.size() > MAX_CACHED_RESULTS) {
307                mRecentResults.remove();
308            }
309        }
310    };
311
312    private void setUpUI(Context context) {
313        String paneName =
314                String.format(Locale.US, "%s %c",
315                        context.getResources().getString(R.string.camera_pane_title),
316                        (char) ('A' + mPaneId));
317        this.setName(paneName);
318
319        LayoutInflater inflater =
320                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
321
322        inflater.inflate(R.layout.camera_pane, this);
323
324        mCameraSpinner = (Spinner) findViewById(R.id.camera_pane_camera_spinner);
325        mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
326
327        mOpenButton = (ToggleButton) findViewById(R.id.camera_pane_open_button);
328        mOpenButton.setOnCheckedChangeListener(mOpenButtonListener);
329        mBaseControls.add(mOpenButton);
330
331        mInfoButton = (Button) findViewById(R.id.camera_pane_info_button);
332        mInfoButton.setOnClickListener(mInfoButtonListener);
333        mBaseControls.add(mInfoButton);
334
335        mStatusText = (TextView) findViewById(R.id.camera_pane_status_text);
336
337        mConfigureButton = (Button) findViewById(R.id.camera_pane_configure_button);
338        mConfigureButton.setOnClickListener(mConfigureButtonListener);
339        mOpenControls.add(mConfigureButton);
340
341        mStopButton = (Button) findViewById(R.id.camera_pane_stop_button);
342        mStopButton.setOnClickListener(mStopButtonListener);
343        mConfiguredControls.add(mStopButton);
344        mFlushButton = (Button) findViewById(R.id.camera_pane_flush_button);
345        mFlushButton.setOnClickListener(mFlushButtonListener);
346        mConfiguredControls.add(mFlushButton);
347    }
348
349    private void initializeCameras(TestingCamera21 tc) {
350        mCameraOps = tc.getCameraOps();
351        mInfoDisplayer = tc;
352
353        updateCameraList();
354    }
355
356    private void updateCameraList() {
357        mCameraIds = null;
358        try {
359            mCameraIds = mCameraOps.getCamerasAndListen(mCameraAvailabilityCallback);
360            String[] cameraSpinnerItems = new String[mCameraIds.length];
361            for (int i = 0; i < mCameraIds.length; i++) {
362                cameraSpinnerItems[i] = String.format("Camera %s", mCameraIds[i]);
363            }
364            mCameraSpinner.setAdapter(new ArrayAdapter<String>(getContext(), R.layout.spinner_item,
365                    cameraSpinnerItems));
366
367        } catch (CameraAccessException e) {
368            TLog.e("Exception trying to get list of cameras: " + e);
369        }
370    }
371
372    private final CompoundButton.OnCheckedChangeListener mOpenButtonListener =
373            new CompoundButton.OnCheckedChangeListener() {
374                @Override
375                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
376                    if (isChecked) {
377                        // Open camera
378                        mCurrentCamera = null;
379                        boolean success = mCameraOps.openCamera(mCurrentCameraId, mCameraListener);
380                        buttonView.setChecked(success);
381                    } else {
382                        // Close camera
383                        closeCurrentCamera();
384                    }
385                }
386            };
387
388    private final OnClickListener mInfoButtonListener = new OnClickListener() {
389        @Override
390        public void onClick(View v) {
391            mInfoDisplayer.showCameraInfo(mCurrentCameraId);
392        }
393    };
394
395    private final OnClickListener mStopButtonListener = new OnClickListener() {
396        @Override
397        public void onClick(View v) {
398            if (mCurrentCaptureSession != null) {
399                try {
400                    mCurrentCaptureSession.stopRepeating();
401                } catch (CameraAccessException e) {
402                    TLog.e("Unable to stop repeating request for camera %s.", e, mCurrentCameraId);
403                }
404            }
405        }
406    };
407
408    private final OnClickListener mFlushButtonListener = new OnClickListener() {
409        @Override
410        public void onClick(View v) {
411            if (mCurrentCaptureSession != null) {
412                try {
413                    mCurrentCaptureSession.abortCaptures();
414                } catch (CameraAccessException e) {
415                    TLog.e("Unable to flush camera %s.", e, mCurrentCameraId);
416                }
417            }
418        }
419    };
420
421    private final OnClickListener mConfigureButtonListener = new OnClickListener() {
422        @Override
423        public void onClick(View v) {
424            List<Surface> targetSurfaces = new ArrayList<Surface>();
425            List<TargetControlPane> targetPanes = new ArrayList<TargetControlPane>();
426            for (TargetControlPane targetPane : mPaneTracker.getPanes(TargetControlPane.class)) {
427                Surface target = targetPane.getTargetSurfaceForCameraPane(getPaneName());
428                if (target != null) {
429                    targetSurfaces.add(target);
430                    targetPanes.add(targetPane);
431                }
432            }
433            try {
434                TLog.i("Configuring camera %s with %d surfaces", mCurrentCamera.getId(),
435                        targetSurfaces.size());
436                mActiveCameraCall = CameraCall.CONFIGURE;
437                if (targetSurfaces.size() > 0) {
438                    mCurrentCamera.createCaptureSession(targetSurfaces, mSessionListener, /*handler*/null);
439                } else if (mCurrentCaptureSession != null) {
440                    mCurrentCaptureSession.close();
441                    mCurrentCaptureSession = null;
442                }
443                mConfiguredSurfaces = targetSurfaces;
444                mConfiguredTargetPanes = targetPanes;
445            } catch (CameraAccessException e) {
446                mActiveCameraCall = CameraCall.NONE;
447                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
448            } catch (IllegalArgumentException e) {
449                mActiveCameraCall = CameraCall.NONE;
450                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
451            } catch (IllegalStateException e) {
452                mActiveCameraCall = CameraCall.NONE;
453                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
454            }
455        }
456    };
457
458    private final CameraCaptureSession.StateCallback mSessionListener =
459            new CameraCaptureSession.StateCallback() {
460
461        @Override
462        public void onConfigured(CameraCaptureSession session) {
463            mCurrentCaptureSession = session;
464            TLog.i("Configuration completed for camera %s.", mCurrentCamera.getId());
465
466            setSessionState(SessionState.CONFIGURED);
467        }
468
469        @Override
470        public void onConfigureFailed(CameraCaptureSession session) {
471            mActiveCameraCall = CameraCall.NONE;
472            TLog.e("Configuration failed for camera %s.", mCurrentCamera.getId());
473
474            setSessionState(SessionState.CONFIGURE_FAILED);
475        }
476
477        @Override
478        public void onReady(CameraCaptureSession session) {
479            setSessionState(SessionState.READY);
480        }
481
482        /**
483         * This method is called when the session starts actively processing capture requests.
484         *
485         * <p>If capture requests are submitted prior to {@link #onConfigured} being called,
486         * then the session will start processing those requests immediately after the callback,
487         * and this method will be immediately called after {@link #onConfigured}.
488         *
489         * <p>If the session runs out of capture requests to process and calls {@link #onReady},
490         * then this callback will be invoked again once new requests are submitted for capture.</p>
491         */
492        @Override
493        public void onActive(CameraCaptureSession session) {
494            setSessionState(SessionState.ACTIVE);
495        }
496
497        /**
498         * This method is called when the session is closed.
499         *
500         * <p>A session is closed when a new session is created by the parent camera device,
501         * or when the parent camera device is closed (either by the user closing the device,
502         * or due to a camera device disconnection or fatal error).</p>
503         *
504         * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and
505         * any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called).
506         * However, any in-progress capture requests submitted to the session will be completed
507         * as normal.</p>
508         */
509        @Override
510        public void onClosed(CameraCaptureSession session) {
511            // Ignore closes if the session has been replaced
512            if (mCurrentCaptureSession != null && session != mCurrentCaptureSession) {
513                return;
514            }
515            setSessionState(SessionState.CLOSED);
516        }
517    };
518
519    private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() {
520        @Override
521        public void onClosed(CameraDevice camera) {
522            // Don't change state on close, tracked by callers of close()
523        }
524
525        @Override
526        public void onDisconnected(CameraDevice camera) {
527            setCameraState(CameraState.DISCONNECTED);
528        }
529
530        @Override
531        public void onError(CameraDevice camera, int error) {
532            setCameraState(CameraState.ERROR);
533        }
534
535        @Override
536        public void onOpened(CameraDevice camera) {
537            mCurrentCamera = camera;
538            setCameraState(CameraState.OPENED);
539        }
540    };
541
542    private void switchToCamera(String newCameraId) {
543        closeCurrentCamera();
544
545        mCurrentCameraId = newCameraId;
546
547        if (mCurrentCameraId == null) {
548            setCameraState(CameraState.UNAVAILABLE);
549        } else {
550            setCameraState(CameraState.CLOSED);
551        }
552
553        mPaneTracker.notifyOtherPanes(this, PaneTracker.PaneEvent.NEW_CAMERA_SELECTED);
554    }
555
556    private void closeCurrentCamera() {
557        if (mCurrentCamera != null) {
558            mCurrentCamera.close();
559            mCurrentCamera = null;
560            setCameraState(CameraState.CLOSED);
561            mOpenButton.setChecked(false);
562        }
563    }
564
565    private void setSessionState(SessionState newState) {
566        mSessionState = newState;
567        mStatusText.setText("S." + mSessionState.toString());
568
569        switch (mSessionState) {
570            case CONFIGURE_FAILED:
571                mActiveCameraCall = CameraCall.NONE;
572                // fall-through
573            case CLOSED:
574                enableBaseControls(true);
575                enableOpenControls(true);
576                enableConfiguredControls(false);
577                mConfiguredTargetPanes = null;
578                break;
579            case NONE:
580                enableBaseControls(true);
581                enableOpenControls(true);
582                enableConfiguredControls(false);
583                mConfiguredTargetPanes = null;
584                break;
585            case CONFIGURED:
586                if (mActiveCameraCall != CameraCall.CONFIGURE) {
587                    throw new AssertionError();
588                }
589                mPaneTracker.notifyOtherPanes(this, PaneEvent.CAMERA_CONFIGURED);
590                mActiveCameraCall = CameraCall.NONE;
591                // fall-through
592            case READY:
593            case ACTIVE:
594                enableBaseControls(true);
595                enableOpenControls(true);
596                enableConfiguredControls(true);
597                break;
598            default:
599                throw new AssertionError("Unhandled case " + mSessionState);
600        }
601    }
602
603    private void setCameraState(CameraState newState) {
604        mCameraState = newState;
605        mStatusText.setText("C." + mCameraState.toString());
606        switch (mCameraState) {
607            case UNAVAILABLE:
608                enableBaseControls(false);
609                enableOpenControls(false);
610                enableConfiguredControls(false);
611                mConfiguredTargetPanes = null;
612                break;
613            case CLOSED:
614            case DISCONNECTED:
615            case ERROR:
616                enableBaseControls(true);
617                enableOpenControls(false);
618                enableConfiguredControls(false);
619                mConfiguredTargetPanes = null;
620                break;
621            case OPENED:
622                enableBaseControls(true);
623                enableOpenControls(true);
624                enableConfiguredControls(false);
625                mConfiguredTargetPanes = null;
626                break;
627        }
628    }
629
630    private void enableBaseControls(boolean enabled) {
631        for (View v : mBaseControls) {
632            v.setEnabled(enabled);
633        }
634        if (!enabled) {
635            mOpenButton.setChecked(false);
636        }
637    }
638
639    private void enableOpenControls(boolean enabled) {
640        for (View v : mOpenControls) {
641            v.setEnabled(enabled);
642        }
643    }
644
645    private void enableConfiguredControls(boolean enabled) {
646        for (View v : mConfiguredControls) {
647            v.setEnabled(enabled);
648        }
649    }
650
651    private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback =
652            new CameraManager.AvailabilityCallback() {
653        @Override
654        public void onCameraAvailable(String cameraId) {
655            // TODO: Update camera list in an intelligent fashion
656            // (can't just call updateCameraList or the selected camera may change)
657        }
658
659        @Override
660        public void onCameraUnavailable(String cameraId) {
661            // TODO: Update camera list in an intelligent fashion
662            // (can't just call updateCameraList or the selected camera may change)
663        }
664    };
665
666    private final OnItemSelectedListener mCameraSpinnerListener = new OnItemSelectedListener() {
667        @Override
668        public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
669            String newCameraId = mCameraIds[pos];
670            if (newCameraId != mCurrentCameraId) {
671                switchToCamera(newCameraId);
672            }
673        }
674
675        @Override
676        public void onNothingSelected(AdapterView<?> parent) {
677            switchToCamera(null);
678        }
679    };
680}
681