blob: 869e0658446c2626cf164afd1dd33342e6930ec5 [file] [log] [blame]
/*
* Copyright (C) 2016 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.devcamera;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureResult;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.app.Activity;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
/**
* A minimum camera app.
* To keep it simple: portrait mode only.
*/
public class DevCameraActivity extends Activity implements CameraInterface.MyCameraCallback, SurfaceHolder.Callback {
private static final String TAG = "DevCamera_UI";
private static final boolean LOG_FRAME_DATA = false;
private static final int AF_TRIGGER_HOLD_MILLIS = 4000;
private static final boolean STARTUP_FULL_YUV_ON = true;
private static final boolean START_WITH_FRONT_CAMERA = false;
private static final int PERMISSIONS_REQUEST_CAMERA = 1;
private boolean mPermissionCheckActive = false;
private SurfaceView mPreviewView;
private SurfaceHolder mPreviewHolder;
private PreviewOverlay mPreviewOverlay;
private FrameLayout mPreviewFrame;
private TextView mLabel1;
private TextView mLabel2;
private ToggleButton mToggleFrontCam; // Use front camera
private ToggleButton mToggleYuvFull; // full YUV
private ToggleButton mToggleYuvVga; // VGA YUV
private ToggleButton mToggleRaw; // raw10
private Button mButtonNoiseMode; // Noise reduction mode
private Button mButtonEdgeModeReprocess; // Edge mode
private Button mButtonNoiseModeReprocess; // Noise reduction mode for reprocessing
private Button mButtonEdgeMode; // Edge mode for reprocessing
private ToggleButton mToggleFace; // Face detection
private ToggleButton mToggleShow3A; // 3A info
private ToggleButton mToggleGyro; // Gyro
private ToggleButton mToggleBurstJpeg;
private ToggleButton mToggleSaveSdCard;
private LinearLayout mReprocessingGroup;
private Handler mMainHandler;
private CameraInterface mCamera;
// Used for saving JPEGs.
private HandlerThread mUtilityThread;
private Handler mUtilityHandler;
// send null for initialization
View.OnClickListener mTransferUiStateToCameraState = new View.OnClickListener() {
@Override
public void onClick(View view) {
// set capture flow.
if (view == mToggleYuvFull || view == mToggleYuvVga || view == mToggleRaw ||
view == mButtonNoiseMode || view == mButtonEdgeMode || view == mToggleFace || view == null)
mCamera.setCaptureFlow(
mToggleYuvFull.isChecked(),
mToggleYuvVga.isChecked(),
mToggleRaw.isChecked(),
view == mButtonNoiseMode, /* cycle noise reduction mode */
view == mButtonEdgeMode, /* cycle edge mode */
mToggleFace.isChecked()
);
// set reprocessing flow.
if (view == mButtonNoiseModeReprocess || view == mButtonEdgeModeReprocess || view == null) {
mCamera.setReprocessingFlow(view == mButtonNoiseModeReprocess, view == mButtonEdgeModeReprocess);
}
// set visibility of cluster of reprocessing controls.
int reprocessingViz = mToggleYuvFull.isChecked() && mCamera.isReprocessingAvailable() ? View.VISIBLE : View.GONE;
mReprocessingGroup.setVisibility(reprocessingViz);
// if just turned off YUV1 stream, end burst.
if (view == mToggleYuvFull && !mToggleYuvFull.isChecked()) {
mToggleBurstJpeg.setChecked(false);
mCamera.setBurst(false);
}
if (view == mToggleBurstJpeg) {
mCamera.setBurst(mToggleBurstJpeg.isChecked());
}
if (view == mToggleShow3A || view == null) {
mPreviewOverlay.show3AInfo(mToggleShow3A.isChecked());
}
if (view == mToggleGyro || view == null) {
if (mToggleGyro.isChecked()) {
startGyroDisplay();
} else {
stopGyroDisplay();
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "onCreate");
CameraTimer.t0 = SystemClock.elapsedRealtime();
if (checkPermissions()) {
// Go speed racer.
openCamera(START_WITH_FRONT_CAMERA);
}
// Initialize UI.
setContentView(R.layout.activity_main);
mLabel1 = (TextView) findViewById(R.id.label1);
mLabel1.setText("Snappy initializing.");
mLabel2 = (TextView) findViewById(R.id.label2);
mLabel2.setText(" ...");
Button mAfTriggerButton = (Button) findViewById(R.id.af_trigger);
mToggleFrontCam = (ToggleButton) findViewById(R.id.toggle_front_cam);
mToggleFrontCam.setChecked(START_WITH_FRONT_CAMERA);
mToggleYuvFull = (ToggleButton) findViewById(R.id.toggle_yuv_full);
mToggleYuvVga = (ToggleButton) findViewById(R.id.toggle_yuv_vga);
mToggleRaw = (ToggleButton) findViewById(R.id.toggle_raw);
mButtonNoiseMode = (Button) findViewById(R.id.button_noise);
mButtonEdgeMode = (Button) findViewById(R.id.button_edge);
mButtonNoiseModeReprocess = (Button) findViewById(R.id.button_noise_reprocess);
mButtonEdgeModeReprocess = (Button) findViewById(R.id.button_edge_reprocess);
mToggleFace = (ToggleButton) findViewById(R.id.toggle_face);
mToggleShow3A = (ToggleButton) findViewById(R.id.toggle_show_3A);
mToggleGyro = (ToggleButton) findViewById(R.id.toggle_show_gyro);
Button mGetJpegButton = (Button) findViewById(R.id.jpeg_capture);
Button mGalleryButton = (Button) findViewById(R.id.gallery);
mToggleBurstJpeg = (ToggleButton) findViewById(R.id.toggle_burst_jpeg);
mToggleSaveSdCard = (ToggleButton) findViewById(R.id.toggle_save_sdcard);
mReprocessingGroup = (LinearLayout) findViewById(R.id.reprocessing_controls);
mPreviewView = (SurfaceView) findViewById(R.id.preview_view);
mPreviewHolder = mPreviewView.getHolder();
mPreviewHolder.addCallback(this);
mPreviewOverlay = (PreviewOverlay) findViewById(R.id.preview_overlay_view);
mPreviewFrame = (FrameLayout) findViewById(R.id.preview_frame);
// Set UI listeners.
mAfTriggerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
doAFScan();
}
});
mGetJpegButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
hitCaptureButton();
}
});
mGalleryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchPhotosViewer();
}
});
mToggleFrontCam.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.v(TAG, "switchCamera()");
CameraTimer.t0 = SystemClock.elapsedRealtime();
// ToggleButton isChecked state will determine which camera is started.
openCamera(mToggleFrontCam.isChecked());
startCamera();
}
});
mToggleYuvFull.setOnClickListener(mTransferUiStateToCameraState);
mToggleYuvVga.setOnClickListener(mTransferUiStateToCameraState);
mToggleRaw.setOnClickListener(mTransferUiStateToCameraState);
mButtonNoiseMode.setOnClickListener(mTransferUiStateToCameraState);
mButtonEdgeMode.setOnClickListener(mTransferUiStateToCameraState);
mButtonNoiseModeReprocess.setOnClickListener(mTransferUiStateToCameraState);
mButtonEdgeModeReprocess.setOnClickListener(mTransferUiStateToCameraState);
mToggleFace.setOnClickListener(mTransferUiStateToCameraState);
mToggleShow3A.setOnClickListener(mTransferUiStateToCameraState);
mToggleGyro.setOnClickListener(mTransferUiStateToCameraState);
mToggleBurstJpeg.setOnClickListener(mTransferUiStateToCameraState);
mToggleSaveSdCard.setOnClickListener(mTransferUiStateToCameraState);
mToggleSaveSdCard.setChecked(true);
mMainHandler = new Handler(this.getApplicationContext().getMainLooper());
// General utility thread for e.g. saving JPEGs.
mUtilityThread = new HandlerThread("UtilityThread");
mUtilityThread.start();
mUtilityHandler = new Handler(mUtilityThread.getLooper());
// --- PRINT REPORT ---
//CameraDeviceReport.printReport(this, false);
super.onCreate(savedInstanceState);
}
// Open camera. No UI required.
private void openCamera(boolean frontCamera) {
// Close previous camera if required.
if (mCamera != null) {
mCamera.closeCamera();
}
// --- SET UP CAMERA ---
mCamera = new Api2Camera(this, frontCamera);
mCamera.setCallback(this);
mCamera.openCamera();
}
// Initialize camera related UI and start camera; call openCamera first.
private void startCamera() {
// --- SET UP USER INTERFACE ---
mToggleYuvFull.setChecked(STARTUP_FULL_YUV_ON);
mToggleFace.setChecked(true);
mToggleRaw.setVisibility(mCamera.isRawAvailable() ? View.VISIBLE : View.GONE);
mToggleShow3A.setChecked(true);
mTransferUiStateToCameraState.onClick(null);
// --- SET UP PREVIEW AND OPEN CAMERA ---
if (mPreviewSurfaceValid) {
mCamera.startPreview(mPreviewHolder.getSurface());
} else {
// Note that preview is rotated 90 degrees from camera. We just hard code this now.
Size previewSize = mCamera.getPreviewSize();
// Render in top 12 x 9 of 16 x 9 display.
int renderHeight = 3 * displayHeight() / 4;
int renderWidth = renderHeight * previewSize.getHeight() / previewSize.getWidth();
int renderPad = (displayWidth() - renderWidth) / 2;
mPreviewFrame.setPadding(renderPad, 0, 0, 0);
mPreviewFrame.setLayoutParams(new LinearLayout.LayoutParams(renderWidth + renderPad, renderHeight));
// setFixedSize() will trigger surfaceChanged() callback below, which will start preview.
mPreviewHolder.setFixedSize(previewSize.getHeight(), previewSize.getWidth());
}
}
boolean mPreviewSurfaceValid = false;
@Override
public synchronized void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.v(TAG, String.format("surfaceChanged: format=%x w=%d h=%d", format, width, height));
if (checkPermissions()) {
mPreviewSurfaceValid = true;
mCamera.startPreview(mPreviewHolder.getSurface());
}
}
Runnable mReturnToCafRunnable = new Runnable() {
@Override
public void run() {
mCamera.setCAF();
}
};
private void doAFScan() {
mCamera.triggerAFScan();
mMainHandler.removeCallbacks(mReturnToCafRunnable);
mMainHandler.postDelayed(mReturnToCafRunnable, AF_TRIGGER_HOLD_MILLIS);
}
private int displayWidth() {
DisplayMetrics metrics = new DisplayMetrics();
this.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
return metrics.widthPixels;
}
private int displayHeight() {
DisplayMetrics metrics = new DisplayMetrics();
this.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
return metrics.heightPixels;
}
@Override
public void onStart() {
Log.v(TAG, "onStart");
super.onStart();
// Leave screen on.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (!checkPermissions()) return;
// Can start camera now that we have the above initialized.
if (mCamera == null) {
openCamera(mToggleFrontCam.isChecked());
}
startCamera();
}
private boolean checkPermissions() {
if (mPermissionCheckActive) return false;
// Check for all runtime permissions
if ((checkSelfPermission(Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED )
|| (checkSelfPermission(Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED)
|| (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED)) {
Log.i(TAG, "Requested camera/video permissions");
requestPermissions(new String[] {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_CAMERA);
mPermissionCheckActive = true;
return false;
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
mPermissionCheckActive = false;
if (requestCode == PERMISSIONS_REQUEST_CAMERA) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
Log.i(TAG, "At least one permission denied, can't continue: " + permissions[i]);
finish();
return;
}
}
Log.i(TAG, "All permissions granted");
openCamera(mToggleFrontCam.isChecked());
startCamera();
}
}
@Override
public void onStop() {
Log.v(TAG, "onStop");
if (mCamera != null) {
mCamera.closeCamera();
mCamera = null;
}
// Cancel any pending AF operations.
mMainHandler.removeCallbacks(mReturnToCafRunnable);
stopGyroDisplay(); // No-op if not running.
super.onStop();
}
public void noCamera2Full() {
Toast toast = Toast.makeText(this, "WARNING: this camera does not support camera2 HARDWARE_LEVEL_FULL.", Toast.LENGTH_LONG);
toast.setGravity(Gravity.TOP, 0, 0);
toast.show();
}
@Override
public void setNoiseEdgeText(final String nrMode, final String edgeMode) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mButtonNoiseMode.setText(nrMode);
mButtonEdgeMode.setText(edgeMode);
}
});
}
@Override
public void setNoiseEdgeTextForReprocessing(final String nrMode, final String edgeMode) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mButtonNoiseModeReprocess.setText(nrMode);
mButtonEdgeModeReprocess.setText(edgeMode);
}
});
}
int mJpegCounter = 0;
long mJpegMillis = 0;
@Override
public void jpegAvailable(final byte[] jpegData, final int x, final int y) {
Log.v(TAG, "JPEG returned, size = " + jpegData.length);
long now = SystemClock.elapsedRealtime();
final long dt = mJpegMillis > 0 ? now - mJpegMillis : 0;
mJpegMillis = now;
if (mToggleSaveSdCard.isChecked()) {
mUtilityHandler.post(new Runnable() {
@Override
public void run() {
final String result = MediaSaver.saveJpeg(getApplicationContext(), jpegData, getContentResolver());
mMainHandler.post(new Runnable() {
@Override
public void run() {
fileNameToast(String.format("Saved %dx%d and %d bytes JPEG to %s in %d ms.", x, y, jpegData.length, result, dt));
}
});
}
});
} else {
mMainHandler.post(new Runnable() {
@Override
public void run() {
fileNameToast(String.format("Processing JPEG #%d %dx%d and %d bytes in %d ms.", ++mJpegCounter, x, y, jpegData.length, dt));
}
});
}
}
@Override
public void receivedFirstFrame() {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mPreviewView.setBackgroundColor(Color.TRANSPARENT);
}
});
}
Toast mToast;
public void fileNameToast(String s) {
if (mToast != null) {
mToast.cancel();
}
mToast = Toast.makeText(this, s, Toast.LENGTH_SHORT);
mToast.setGravity(Gravity.TOP, 0, 0);
mToast.show();
}
@Override
public void frameDataAvailable(final NormalizedFace[] faces, final float normExposure, final float normLens, float fps, int iso, final int afState, int aeState, int awbState) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mPreviewOverlay.setFrameData(faces, normExposure, normLens, afState);
}
});
// Build info string.
String ae = aeStateToString(aeState);
String af = afStateToString(afState);
String awb = awbStateToString(awbState);
final String info = String.format(" %2.0f FPS%5d ISO AF:%s AE:%s AWB:%s", fps, iso, af, ae, awb);
mLastInfo = info;
if (LOG_FRAME_DATA && faces != null) {
Log.v(TAG, "normExposure: " + normExposure);
Log.v(TAG, "normLens: " + normLens);
for (int i = 0; i < faces.length; ++i) {
Log.v(TAG, "Face getBounds: " + faces[i].bounds);
Log.v(TAG, "Face left eye: " + faces[i].leftEye);
Log.v(TAG, "Face right eye: " + faces[i].rightEye);
Log.v(TAG, "Face mouth: " + faces[i].mouth);
}
}
// Status line
mMainHandler.post(new Runnable() {
@Override
public void run() {
mLabel1.setText(info);
}
});
}
Integer mTimeToFirstFrame = 0;
Integer mHalWaitTime = 0;
Float mDroppedFrameCount = 0f;
String mLastInfo;
@Override
public void performanceDataAvailable(Integer timeToFirstFrame, Integer halWaitTime, Float droppedFrameCount) {
if (timeToFirstFrame != null) {
mTimeToFirstFrame = timeToFirstFrame;
}
if (halWaitTime != null) {
mHalWaitTime = halWaitTime;
}
if (droppedFrameCount != null) {
mDroppedFrameCount += droppedFrameCount;
}
mMainHandler.post(new Runnable() {
@Override
public void run() {
mLabel2.setText(String.format("TTP %dms HAL %dms Framedrops:%.2f", mTimeToFirstFrame, mHalWaitTime, mDroppedFrameCount));
}
});
}
// Hit capture button.
private void hitCaptureButton() {
Log.v(TAG, "hitCaptureButton");
mCamera.takePicture();
}
// Hit Photos button.
private void launchPhotosViewer() {
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
intent.setType("image/*");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
/*********************************
* Gyro graphics overlay update. *
*********************************/
GyroOperations mGyroOperations;
private void startGyroDisplay() {
float[] fovs = mCamera.getFieldOfView();
mPreviewOverlay.setFieldOfView(fovs[0], fovs[1]);
mPreviewOverlay.setFacingAndOrientation(mToggleFrontCam.isChecked() ?
CameraCharacteristics.LENS_FACING_FRONT : CameraCharacteristics.LENS_FACING_BACK,
mCamera.getOrientation());
if (mGyroOperations == null) {
SensorManager sensorManager = (SensorManager) getSystemService(this.SENSOR_SERVICE);
mGyroOperations = new GyroOperations(sensorManager);
}
mGyroOperations.startListening(
new GyroListener() {
@Override
public void updateGyroAngles(float[] gyroAngles) {
mPreviewOverlay.setGyroAngles(gyroAngles);
}
}
);
mPreviewOverlay.showGyroGrid(true);
}
private void stopGyroDisplay() {
if (mGyroOperations != null) {
mGyroOperations.stopListening();
}
mPreviewOverlay.showGyroGrid(false);
}
/*******************************************
* SurfaceView callbacks just for logging. *
*******************************************/
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v(TAG, "surfaceCreated");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v(TAG, "surfaceDestroyed");
}
/*********************
* UTILITY FUNCTIONS *
*********************/
private static String awbStateToString(int mode) {
switch (mode) {
case CaptureResult.CONTROL_AWB_STATE_INACTIVE:
return "inactive";
case CaptureResult.CONTROL_AWB_STATE_SEARCHING:
return "searching";
case CaptureResult.CONTROL_AWB_STATE_CONVERGED:
return "converged";
case CaptureResult.CONTROL_AWB_STATE_LOCKED:
return "lock";
default:
return "unknown " + Integer.toString(mode);
}
}
private static String aeStateToString(int mode) {
switch (mode) {
case CaptureResult.CONTROL_AE_STATE_INACTIVE:
return "inactive";
case CaptureResult.CONTROL_AE_STATE_SEARCHING:
return "searching";
case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:
return "precapture";
case CaptureResult.CONTROL_AE_STATE_CONVERGED:
return "converged";
case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
return "flashReq";
case CaptureResult.CONTROL_AE_STATE_LOCKED:
return "lock";
default:
return "unknown " + Integer.toString(mode);
}
}
private static String afStateToString(int mode) {
switch (mode) {
case CaptureResult.CONTROL_AF_STATE_INACTIVE:
return "inactive";
case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
return "passiveScan";
case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
return "passiveFocused";
case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:
return "passiveUnfocused";
case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN:
return "activeScan";
case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
return "focusedLock";
case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:
return "notFocusedLock";
default:
return "unknown" + Integer.toString(mode);
}
}
}