blob: b4c05011e230d82d80f2fbfcab3ef4db99106e18 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.testingcamera2;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.TotalCaptureResult;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import com.android.testingcamera2.PaneTracker.PaneEvent;
import java.io.IOException;
/**
*
* Basic control pane block for the control list
*
*/
public class CameraControlPane extends ControlPane {
// XML attributes
/** Name of pane tag */
private static final String PANE_NAME = "camera_pane";
/** Attribute: ID for pane (integer) */
private static final String PANE_ID = "id";
/** Attribute: ID for camera to select (String) */
private static final String CAMERA_ID = "camera_id";
// End XML attributes
private static final int MAX_CACHED_RESULTS = 100;
private static int mCameraPaneIdCounter = 0;
/**
* These correspond to the callbacks from
* android.hardware.camera2.CameraDevice.StateCallback, plus UNAVAILABLE for
* when there's not a valid camera selected.
*/
private enum CameraState {
UNAVAILABLE,
CLOSED,
OPENED,
DISCONNECTED,
ERROR
}
/**
* These correspond to the callbacks from {@link CameraCaptureSession.StateCallback}, plus
* {@code CONFIGURING} for before a session is returned and {@code NONE} for when there
* is no session created.
*/
private enum SessionState {
NONE,
CONFIGURED,
CONFIGURE_FAILED,
READY,
ACTIVE,
CLOSED
}
private enum CameraCall {
NONE,
CONFIGURE
}
private final int mPaneId;
private CameraOps2 mCameraOps;
private InfoDisplayer mInfoDisplayer;
private Spinner mCameraSpinner;
private ToggleButton mOpenButton;
private Button mInfoButton;
private TextView mStatusText;
private Button mConfigureButton;
private Button mStopButton;
private Button mFlushButton;
/**
* All controls that should be enabled when there's a valid camera ID
* selected
*/
private final Set<View> mBaseControls = new HashSet<View>();
/**
* All controls that should be enabled when camera is at least in the OPEN
* state
*/
private final Set<View> mOpenControls = new HashSet<View>();
/**
* All controls that should be enabled when camera is at least in the IDLE
* state
*/
private final Set<View> mConfiguredControls = new HashSet<View>();
private String[] mCameraIds;
private String mCurrentCameraId;
private CameraState mCameraState;
private CameraDevice mCurrentCamera;
private CameraCaptureSession mCurrentCaptureSession;
private SessionState mSessionState = SessionState.NONE;
private CameraCall mActiveCameraCall;
private LinkedList<TotalCaptureResult> mRecentResults = new LinkedList<>();
private List<Surface> mConfiguredSurfaces;
private List<TargetControlPane> mConfiguredTargetPanes;
/**
* Constructor for tooling only
*/
public CameraControlPane(Context context, AttributeSet attrs) {
super(context, attrs, null, null);
mPaneId = 0;
setUpUI(context);
}
public CameraControlPane(TestingCamera21 tc, AttributeSet attrs, StatusListener listener) {
super(tc, attrs, listener, tc.getPaneTracker());
mPaneId = mCameraPaneIdCounter++;
setUpUI(tc);
initializeCameras(tc);
if (mCameraIds != null) {
switchToCamera(mCameraIds[0]);
}
}
public CameraControlPane(TestingCamera21 tc, XmlPullParser configParser, StatusListener listener)
throws XmlPullParserException, IOException {
super(tc, null, listener, tc.getPaneTracker());
configParser.require(XmlPullParser.START_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);
int paneId = getAttributeInt(configParser, PANE_ID, -1);
if (paneId == -1) {
mPaneId = mCameraPaneIdCounter++;
} else {
mPaneId = paneId;
if (mPaneId >= mCameraPaneIdCounter) {
mCameraPaneIdCounter = mPaneId + 1;
}
}
String cameraId = getAttributeString(configParser, CAMERA_ID, null);
configParser.next();
configParser.require(XmlPullParser.END_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);
setUpUI(tc);
initializeCameras(tc);
boolean gotCamera = false;
if (mCameraIds != null && cameraId != null) {
for (int i = 0; i < mCameraIds.length; i++) {
if (cameraId.equals(mCameraIds[i])) {
switchToCamera(mCameraIds[i]);
mCameraSpinner.setSelection(i);
gotCamera = true;
}
}
}
if (!gotCamera && mCameraIds != null) {
switchToCamera(mCameraIds[0]);
}
}
@Override
public void remove() {
closeCurrentCamera();
super.remove();
}
/**
* Get list of target panes that are currently actively configured for this
* camera
*/
public List<TargetControlPane> getCurrentConfiguredTargets() {
return mConfiguredTargetPanes;
}
/**
* Interface to be implemented by an application service for displaying a
* camera's information.
*/
public interface InfoDisplayer {
public void showCameraInfo(String cameraId);
}
public CameraCharacteristics getCharacteristics() {
if (mCurrentCameraId != null) {
return mCameraOps.getCameraInfo(mCurrentCameraId);
}
return null;
}
public CaptureRequest.Builder getRequestBuilder(int template) {
CaptureRequest.Builder request = null;
if (mCurrentCamera != null) {
try {
request = mCurrentCamera.createCaptureRequest(template);
// Workaround for b/15748139
request.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON);
} catch (CameraAccessException e) {
TLog.e("Unable to build request for camera %s with template %d.", e,
mCurrentCameraId, template);
}
}
return request;
}
/**
* Send single capture to camera device.
*
* @param request
* @return true if capture sent successfully
*/
public boolean capture(CaptureRequest request) {
if (mCurrentCaptureSession != null) {
try {
mCurrentCaptureSession.capture(request, mResultListener, null);
return true;
} catch (CameraAccessException e) {
TLog.e("Unable to capture for camera %s.", e, mCurrentCameraId);
}
}
return false;
}
public boolean repeat(CaptureRequest request) {
if (mCurrentCaptureSession != null) {
try {
mCurrentCaptureSession.setRepeatingRequest(request, mResultListener, null);
return true;
} catch (CameraAccessException e) {
TLog.e("Unable to set repeating request for camera %s.", e, mCurrentCameraId);
}
}
return false;
}
public TotalCaptureResult getResultAt(long timestamp) {
for (TotalCaptureResult result : mRecentResults) {
long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
if (resultTimestamp == timestamp) return result;
if (resultTimestamp > timestamp) return null;
}
return null;
}
public void prepareSurface(Surface target) {
if (mCurrentCaptureSession != null) {
try {
TLog.i("Preparing Surface " + target);
mCurrentCaptureSession.prepare(target);
} catch (CameraAccessException e) {
TLog.e("Unable to prepare surface for camera %s.", e, mCurrentCameraId);
} catch (IllegalArgumentException e) {
TLog.e("Bad Surface passed to prepare", e);
}
}
}
private CaptureCallback mResultListener = new CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session,
CaptureRequest request, long timestamp, long frameNumber) {
}
@Override
public void onCaptureProgressed(CameraCaptureSession session,
CaptureRequest request, CaptureResult partialResult) {
}
@Override
public void onCaptureCompleted(
CameraCaptureSession session,
CaptureRequest request,
TotalCaptureResult result) {
mRecentResults.add(result);
if (mRecentResults.size() > MAX_CACHED_RESULTS) {
mRecentResults.remove();
}
}
@Override
public void onCaptureFailed(CameraCaptureSession session,
CaptureRequest request, CaptureFailure failure) {
TLog.e("Capture failed for request " + request +
" on frame " + failure.getFrameNumber() + ": Reason " + failure.getReason() +
". Images captured: " + failure.wasImageCaptured());
}
@Override
public void onCaptureSequenceCompleted(CameraCaptureSession session,
int sequenceId, long frameNumber) {
}
@Override
public void onCaptureSequenceAborted(CameraCaptureSession session,
int sequenceId) {
}
@Override
public void onCaptureBufferLost(CameraCaptureSession session,
CaptureRequest request, Surface target, long frameNumber) {
TLog.e("Lost buffer for Surface " + target + " for request " + request +
" on frame " + frameNumber);
}
};
private void setUpUI(Context context) {
String paneName =
String.format(Locale.US, "%s %c",
context.getResources().getString(R.string.camera_pane_title),
(char) ('A' + mPaneId));
this.setName(paneName);
LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.camera_pane, this);
mCameraSpinner = (Spinner) findViewById(R.id.camera_pane_camera_spinner);
mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
mOpenButton = (ToggleButton) findViewById(R.id.camera_pane_open_button);
mOpenButton.setOnCheckedChangeListener(mOpenButtonListener);
mBaseControls.add(mOpenButton);
mInfoButton = (Button) findViewById(R.id.camera_pane_info_button);
mInfoButton.setOnClickListener(mInfoButtonListener);
mBaseControls.add(mInfoButton);
mStatusText = (TextView) findViewById(R.id.camera_pane_status_text);
mConfigureButton = (Button) findViewById(R.id.camera_pane_configure_button);
mConfigureButton.setOnClickListener(mConfigureButtonListener);
mOpenControls.add(mConfigureButton);
mStopButton = (Button) findViewById(R.id.camera_pane_stop_button);
mStopButton.setOnClickListener(mStopButtonListener);
mConfiguredControls.add(mStopButton);
mFlushButton = (Button) findViewById(R.id.camera_pane_flush_button);
mFlushButton.setOnClickListener(mFlushButtonListener);
mConfiguredControls.add(mFlushButton);
}
private void initializeCameras(TestingCamera21 tc) {
mCameraOps = tc.getCameraOps();
mInfoDisplayer = tc;
updateCameraList();
}
private void updateCameraList() {
mCameraIds = null;
try {
mCameraIds = mCameraOps.getCamerasAndListen(mCameraAvailabilityCallback);
String[] cameraSpinnerItems = new String[mCameraIds.length];
for (int i = 0; i < mCameraIds.length; i++) {
cameraSpinnerItems[i] = String.format("Camera %s", mCameraIds[i]);
}
mCameraSpinner.setAdapter(new ArrayAdapter<String>(getContext(), R.layout.spinner_item,
cameraSpinnerItems));
} catch (CameraAccessException e) {
TLog.e("Exception trying to get list of cameras: " + e);
}
}
private final CompoundButton.OnCheckedChangeListener mOpenButtonListener =
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
// Open camera
mCurrentCamera = null;
mCameraOps.openCamera(mCurrentCameraId, mCameraListener);
} else {
// Close camera
closeCurrentCamera();
}
}
};
private final OnClickListener mInfoButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
mInfoDisplayer.showCameraInfo(mCurrentCameraId);
}
};
private final OnClickListener mStopButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentCaptureSession != null) {
try {
mCurrentCaptureSession.stopRepeating();
} catch (CameraAccessException e) {
TLog.e("Unable to stop repeating request for camera %s.", e, mCurrentCameraId);
}
}
}
};
private final OnClickListener mFlushButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentCaptureSession != null) {
try {
mCurrentCaptureSession.abortCaptures();
} catch (CameraAccessException e) {
TLog.e("Unable to flush camera %s.", e, mCurrentCameraId);
}
}
}
};
private final OnClickListener mConfigureButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
List<Surface> targetSurfaces = new ArrayList<Surface>();
List<TargetControlPane> targetPanes = new ArrayList<TargetControlPane>();
for (TargetControlPane targetPane : mPaneTracker.getPanes(TargetControlPane.class)) {
Surface target = targetPane.getTargetSurfaceForCameraPane(getPaneName());
if (target != null) {
targetSurfaces.add(target);
targetPanes.add(targetPane);
}
}
try {
TLog.i("Configuring camera %s with %d surfaces", mCurrentCamera.getId(),
targetSurfaces.size());
mActiveCameraCall = CameraCall.CONFIGURE;
if (targetSurfaces.size() > 0) {
mCurrentCamera.createCaptureSession(targetSurfaces, mSessionListener, /*handler*/null);
} else if (mCurrentCaptureSession != null) {
mCurrentCaptureSession.close();
mCurrentCaptureSession = null;
}
mConfiguredSurfaces = targetSurfaces;
mConfiguredTargetPanes = targetPanes;
} catch (CameraAccessException e) {
mActiveCameraCall = CameraCall.NONE;
TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
} catch (IllegalArgumentException e) {
mActiveCameraCall = CameraCall.NONE;
TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
} catch (IllegalStateException e) {
mActiveCameraCall = CameraCall.NONE;
TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
}
}
};
private final CameraCaptureSession.StateCallback mSessionListener =
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
mCurrentCaptureSession = session;
TLog.i("Configuration completed for camera %s.", mCurrentCamera.getId());
setSessionState(SessionState.CONFIGURED);
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
mActiveCameraCall = CameraCall.NONE;
TLog.e("Configuration failed for camera %s.", mCurrentCamera.getId());
setSessionState(SessionState.CONFIGURE_FAILED);
}
@Override
public void onReady(CameraCaptureSession session) {
setSessionState(SessionState.READY);
}
/**
* This method is called when the session starts actively processing capture requests.
*
* <p>If capture requests are submitted prior to {@link #onConfigured} being called,
* then the session will start processing those requests immediately after the callback,
* and this method will be immediately called after {@link #onConfigured}.
*
* <p>If the session runs out of capture requests to process and calls {@link #onReady},
* then this callback will be invoked again once new requests are submitted for capture.</p>
*/
@Override
public void onActive(CameraCaptureSession session) {
setSessionState(SessionState.ACTIVE);
}
/**
* This method is called when the session is closed.
*
* <p>A session is closed when a new session is created by the parent camera device,
* or when the parent camera device is closed (either by the user closing the device,
* or due to a camera device disconnection or fatal error).</p>
*
* <p>Once a session is closed, all methods on it will throw an IllegalStateException, and
* any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called).
* However, any in-progress capture requests submitted to the session will be completed
* as normal.</p>
*/
@Override
public void onClosed(CameraCaptureSession session) {
// Ignore closes if the session has been replaced
if (mCurrentCaptureSession != null && session != mCurrentCaptureSession) {
return;
}
setSessionState(SessionState.CLOSED);
}
@Override
public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
TLog.i("Surface preparation complete for Surface " + surface);
}
};
private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() {
@Override
public void onClosed(CameraDevice camera) {
// Don't change state on close, tracked by callers of close()
mOpenButton.setChecked(false);
}
@Override
public void onDisconnected(CameraDevice camera) {
setCameraState(CameraState.DISCONNECTED);
}
@Override
public void onError(CameraDevice camera, int error) {
setCameraState(CameraState.ERROR);
}
@Override
public void onOpened(CameraDevice camera) {
mCurrentCamera = camera;
setCameraState(CameraState.OPENED);
}
};
private void switchToCamera(String newCameraId) {
closeCurrentCamera();
mCurrentCameraId = newCameraId;
if (mCurrentCameraId == null) {
setCameraState(CameraState.UNAVAILABLE);
} else {
setCameraState(CameraState.CLOSED);
}
mPaneTracker.notifyOtherPanes(this, PaneTracker.PaneEvent.NEW_CAMERA_SELECTED);
}
private void closeCurrentCamera() {
if (mCurrentCamera != null) {
mCurrentCamera.close();
mCurrentCamera = null;
setCameraState(CameraState.CLOSED);
}
}
private void setSessionState(SessionState newState) {
mSessionState = newState;
mStatusText.setText("S." + mSessionState.toString());
switch (mSessionState) {
case CONFIGURE_FAILED:
mActiveCameraCall = CameraCall.NONE;
// fall-through
case CLOSED:
enableBaseControls(true);
enableOpenControls(true);
enableConfiguredControls(false);
mConfiguredTargetPanes = null;
break;
case NONE:
enableBaseControls(true);
enableOpenControls(true);
enableConfiguredControls(false);
mConfiguredTargetPanes = null;
break;
case CONFIGURED:
if (mActiveCameraCall != CameraCall.CONFIGURE) {
throw new AssertionError();
}
mPaneTracker.notifyOtherPanes(this, PaneEvent.CAMERA_CONFIGURED);
mActiveCameraCall = CameraCall.NONE;
// fall-through
case READY:
case ACTIVE:
enableBaseControls(true);
enableOpenControls(true);
enableConfiguredControls(true);
break;
default:
throw new AssertionError("Unhandled case " + mSessionState);
}
}
private void setCameraState(CameraState newState) {
mCameraState = newState;
mStatusText.setText("C." + mCameraState.toString());
switch (mCameraState) {
case UNAVAILABLE:
enableBaseControls(false);
enableOpenControls(false);
enableConfiguredControls(false);
mConfiguredTargetPanes = null;
break;
case CLOSED:
case DISCONNECTED:
case ERROR:
enableBaseControls(true);
enableOpenControls(false);
enableConfiguredControls(false);
mConfiguredTargetPanes = null;
break;
case OPENED:
enableBaseControls(true);
enableOpenControls(true);
enableConfiguredControls(false);
mConfiguredTargetPanes = null;
break;
}
}
private void enableBaseControls(boolean enabled) {
for (View v : mBaseControls) {
v.setEnabled(enabled);
}
if (!enabled) {
mOpenButton.setChecked(false);
}
}
private void enableOpenControls(boolean enabled) {
for (View v : mOpenControls) {
v.setEnabled(enabled);
}
}
private void enableConfiguredControls(boolean enabled) {
for (View v : mConfiguredControls) {
v.setEnabled(enabled);
}
}
private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback =
new CameraManager.AvailabilityCallback() {
@Override
public void onCameraAvailable(String cameraId) {
// TODO: Update camera list in an intelligent fashion
// (can't just call updateCameraList or the selected camera may change)
}
@Override
public void onCameraUnavailable(String cameraId) {
// TODO: Update camera list in an intelligent fashion
// (can't just call updateCameraList or the selected camera may change)
}
};
private final OnItemSelectedListener mCameraSpinnerListener = new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
String newCameraId = mCameraIds[pos];
if (newCameraId != mCurrentCameraId) {
switchToCamera(newCameraId);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
switchToCamera(null);
}
};
}