Compare commits

..

4 commits
fix ... master

28 changed files with 704 additions and 1213 deletions

View file

@ -48,5 +48,4 @@ dependencies {
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
testImplementation libs.robolectric
}

View file

@ -7,8 +7,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
android:maxSdkVersion="32" />
<application
android:allowBackup="true"
@ -20,12 +19,6 @@
android:supportsRtl="true"
android:theme="@style/Theme.CameraXTestAppJava"
tools:targetApi="31">
<activity
android:name=".CameraActivityNew"
android:exported="false" />
<activity
android:name=".CameraActivityOld"
android:exported="false" />
<activity
android:name=".Camera2Activity"
android:exported="false" />

View file

@ -1,124 +0,0 @@
package com.example.cameraxtestappjava;
import static com.example.cameraxtestappjava.segpass.SegpassCamera.REQUEST_CAMERA_PERMISSION;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import com.example.cameraxtestappjava.segpass.camera.view.AutoFitTextureView;
import com.example.cameraxtestappjava.segpass.camera.utils.SegpassCameraCallback;
import com.example.cameraxtestappjava.segpass.camera.utils.SegpassPermissionCallback;
import com.example.cameraxtestappjava.databinding.ActivityCameraNewBinding;
import com.example.cameraxtestappjava.segpass.SegpassCamera;
public class CameraActivityNew extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback, SegpassCameraCallback, SegpassPermissionCallback {
private static final String TAG = "CameraActivityNew";
ActivityCameraNewBinding binding;
SegpassCamera mSegpassCamera;
AutoFitTextureView mTextureView;
@SuppressLint("SourceLockedOrientationActivity")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivityCameraNewBinding.inflate(getLayoutInflater());
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(binding.getRoot());
mTextureView = binding.inCamera2.tvCameraTextureView;
mSegpassCamera = new SegpassCamera(this, mTextureView, this, this);
setUpListeners();
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
@Override
public void onResume() {
super.onResume();
mSegpassCamera.resumeCamera();
}
@Override
public void onPause() {
mSegpassCamera.pauseCamera();
super.onPause();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.length != 2 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
onPermissionDenied();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private void setUpListeners() {
binding.inCamera2.btnTakepicture.setOnClickListener(v -> mSegpassCamera.takePicture());
}
/**
* Camera callback
*/
@Override
public void onPictureTakenSuccess(String base64) {
mSegpassCamera.showToast("Base64 generated.");
}
@Override
public void onPictureTakenFailError(String error) {
mSegpassCamera.showToast(error);
}
/**
* Camera state callback (just to validate if there was an error while initializing the camera)
*/
@Override
public void onCameraInitError(String errorMessage) {
mSegpassCamera.showToast(errorMessage);
}
/**
* Permissions callbck
*/
@Override
public void onPermissionRequest() {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CAMERA_PERMISSION);
}
@Override
public void onPermissionGranted() {
// Do nothing, use camera.
}
@Override
public void onPermissionDenied() {
mSegpassCamera.showToast("No permissions granted, closing camera.");
finish();
}
}

View file

@ -1,35 +0,0 @@
package com.example.cameraxtestappjava;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.example.cameraxtestappjava.databinding.ActivityCameraBinding;
public class CameraActivityOld extends AppCompatActivity {
ActivityCameraBinding binding;
private static final String TAG = "CameraActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivityCameraBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
}

View file

@ -1,24 +1,35 @@
package com.example.cameraxtestappjava;
import android.content.Intent;
import static com.example.cameraxtestappjava.camera.SegpassCamera.REQUEST_CAMERA_PERMISSION;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.TextureView;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.example.cameraxtestappjava.camera.SegpassPermissionListener;
import com.example.cameraxtestappjava.camera.SegpassCamera;
import com.example.cameraxtestappjava.camera.SegpassCameraCallback;
import com.example.cameraxtestappjava.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
public class MainActivity extends AppCompatActivity implements SegpassPermissionListener {
// binding
ActivityMainBinding binding;
// Camera2
private static final String TAG = "MainActivity";
SegpassCamera mSegpassCamera;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -28,7 +39,7 @@ public class MainActivity extends AppCompatActivity {
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setListeners();
setUpCamera2();
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
@ -37,17 +48,75 @@ public class MainActivity extends AppCompatActivity {
});
}
private void setListeners() {
binding.goToCamera.setOnClickListener(v -> {
Intent intent = new Intent(this, CameraActivityNew.class);
startActivity(intent);
});
// Camera 2
private void setUpCamera2() {
// Initialize visual elements in host app
TextureView mTextureView = binding.c2Camera.tvCameraTextureView;
ImageButton mTakePictureButton = binding.c2Camera.btnTakepicture;
// Initialized CustomCamera2 in host app
//mCustomCamera2 = new CustomCamera2(this, mTextureView);
mSegpassCamera = new SegpassCamera(this, mTextureView, this);
mSegpassCamera.init(); // Do not skip this!!!
mTakePictureButton.setOnClickListener(v -> mSegpassCamera.takePicture(new SegpassCameraCallback() {
@Override
public void onPictureTakenSuccess(String message) {
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
}
@Override
public void onPictureTakenFailError(String error) {
Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
}
}));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
// close the app
Toast.makeText(MainActivity.this, "Sorry!!!, you can't use this app without granting permission", Toast.LENGTH_LONG).show();
finish();
} else {
onPermissionGranted();
}
}
}
@Override
protected void onResume() {
super.onResume();
setListeners();
mSegpassCamera.resumeCamera();
}
@Override
protected void onPause() {
mSegpassCamera.pauseCamera();
super.onPause();
}
@Override
protected void onDestroy() {
mSegpassCamera.destroyCamera();
super.onDestroy();
}
@Override
public void onPermissionGranted() {
Toast.makeText(MainActivity.this, "You have permissions to use the camera!", Toast.LENGTH_LONG).show();
}
@Override
public void onPermissionDenied() {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.CAMERA,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_CAMERA_PERMISSION
);
}
}

View file

@ -0,0 +1,515 @@
package com.example.cameraxtestappjava.camera;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class SegpassCamera {
private static final String TAG = "SegpassCamera";
AppCompatActivity mActivity;
private SegpassPermissionListener mPermissionListener;
private TextureView mTextureView;
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
private String mCameraId;
private String mDefaultCameraId = "0";
private Size mPreviewSize;
private int mTotalRotation;
protected CameraDevice mCameraDevice;
protected String[] mCameraIds;
protected CameraManager mCameraManager;
protected CameraCaptureSession mCameraCaptureSessions;
private Size mImageDimension;
private ImageReader mImageReader;
private File mFileFolder;
private File mFile;
public static final int REQUEST_CAMERA_PERMISSION = 200;
private Handler mBackgroundHandler;
private HandlerThread mBackgroundThread;
private CaptureRequest.Builder mCaptureRequestBuilder;
private static class CompareSizeByArea implements Comparator<Size> {
@Override
public int compare(Size ths, Size rhs) {
return Long.signum((long) ths.getWidth() * ths.getHeight() /
(long) rhs.getWidth() * rhs.getHeight());
}
}
public SegpassCamera(AppCompatActivity activity, TextureView textureView, SegpassPermissionListener listener) {
mActivity = activity;
mTextureView = textureView;
mPermissionListener = listener;
}
public void init() {
createFolder();
setUpListeners();
}
private void setUpListeners() {
mTextureView.setSurfaceTextureListener(textureListener);
}
public TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
//open your camera here
Log.d(TAG, "TextureView.SurfaceTextureListener.onSurfaceTextureAvailable(): the TextureView is available.");
setupCamera(width, height);
connectCamera();
startPreview();
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
// Transform you image captured size according to the surface width and height
Log.d(TAG, "TextureView.SurfaceTextureListener.onSurfaceTextureSizeChanged(): the TextureView size has been updated.");
closeCamera();
setupCamera(width, height);
connectCamera();
startPreview();
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
Log.d(TAG, "TextureView.SurfaceTextureListener.onSurfaceTextureDestroyed(): the TextureView has beed destroyed.");
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
// No code implementation needed for this use case.
}
};
// Camera Device State Callback
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
Log.d(TAG, "CameraDevice.StateCallback.onOpened(): camera init/open.");
mCameraDevice = camera;
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
Log.d(TAG, "CameraDevice.StateCallback.onDisconnected(): camera close.");
mCameraDevice.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
Log.d(TAG, "CameraDevice.StateCallback.onError(): camera close. ");
mCameraDevice.close();
mCameraDevice = null;
}
};
//Background Thread
public void startBackgroundThread() {
Log.d(TAG, "startBackgroundThread()...");
mBackgroundThread = new HandlerThread("Camera Background");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
public void stopBackgroundThread() {
Log.d(TAG, "stopBackgroundThread()...");
mBackgroundThread.quitSafely();
try {
mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
} catch (InterruptedException e) {
Log.e(TAG, "stopBackgroundThread() - InterruptedException: " + e.getMessage());
}
}
// Setup Camera
private CameraCharacteristics getCameraCharacteristics() {
try {
for (String cameraId : mCameraIds) {
// Local cc
CameraCharacteristics cc = mCameraManager.getCameraCharacteristics(cameraId);
// If no LENS_FACING characteristic is found, NullPointerException
if (cc.get(CameraCharacteristics.LENS_FACING) == null)
throw new NullPointerException();
if (cc.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
mCameraId = cameraId;
} else {
mCameraId = mDefaultCameraId;
}
}
return mCameraManager.getCameraCharacteristics(mCameraId);
} catch (NullPointerException | CameraAccessException e) {
Log.e(TAG, "getCameraCharacteristics() Exception: " + e.getMessage());
return null;
}
}
public void setupCamera(int width, int height) {
mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
try {
CameraCharacteristics cameraCharacteristics = getCameraCharacteristics();
// If no characteristics are found, throw a CameraAccessException
if (cameraCharacteristics == null)
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
// Shenanigans
StreamConfigurationMap map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
int deviceOrientation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
mTotalRotation = sensorToDeviceRotation(cameraCharacteristics, deviceOrientation);
boolean swapRotation = mTotalRotation == 90 || mTotalRotation == 270;
int rotatedWidth = width;
int rotatedHeight = height;
if (swapRotation) {
rotatedWidth = height;
rotatedHeight = width;
}
assert map != null;
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), rotatedWidth, rotatedHeight);
} catch (CameraAccessException e) {
Log.e(TAG, "setupCamera() Exception: " + e.getMessage());
}
}
//Connect Camera
public void connectCameraNoListener() {
try {
if (ActivityCompat.checkSelfPermission(mActivity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(mActivity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(mActivity, new String[]{android.Manifest.permission.CAMERA, android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CAMERA_PERMISSION);
return;
}
mCameraManager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "connectCamera() CameraAccessException: " + e.getMessage());
} catch (SecurityException e) {
Log.e(TAG, "connectCamera() SecurityException: " + e.getMessage());
}
}
public void connectCamera() {
try {
if (ActivityCompat.checkSelfPermission(mActivity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(mActivity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
) {
mPermissionListener.onPermissionDenied();
return;
}
mCameraManager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "connectCamera() CameraAccessException: " + e.getMessage());
} catch (SecurityException e) {
Log.e(TAG, "connectCamera() SecurityException: " + e.getMessage());
}
}
// Start Preview
public void startPreview() {
SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
assert surfaceTexture != null;
surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface previewSurface = new Surface(surfaceTexture);
try {
if (mCameraDevice == null) throw new NullPointerException("Camera not started!");
mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mCaptureRequestBuilder.addTarget(previewSurface);
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON);
mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
try {
cameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "Camera can be accessed.");
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Toast.makeText(mActivity, "Configuration change", Toast.LENGTH_SHORT).show();
}
}, null);
} catch (CameraAccessException e) {
Log.e(TAG, "startPreview(): " + e.getMessage());
} catch (NullPointerException | IllegalStateException e) {
Log.e(TAG, "startPreview(): " + e.getMessage());
resumeCamera();
}
}
//Take Picture
public void takePicture(SegpassCameraCallback cameraCallback) {
if (mCameraDevice == null) {
Log.e(TAG, "cameraDevice is null");
return;
}
try {
CameraCharacteristics characteristics = getCameraCharacteristics();
if (characteristics == null)
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
Size[] jpegSizes = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(ImageFormat.JPEG);
if (jpegSizes == null) throw new NullPointerException("Error taking picture");
int width = 640;
int height = 480;
if (jpegSizes.length > 0) {
width = jpegSizes[0].getWidth();
height = jpegSizes[0].getHeight();
}
ImageReader mReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
List<Surface> outputSurfaces = new ArrayList<Surface>(2);
outputSurfaces.add(mReader.getSurface());
outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture()));
final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
// Orientation
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
String fileName = "IMG_" + System.currentTimeMillis() + ".jpg";
mFile = new File(mFileFolder + "/" + fileName); // Ver como hacer para guardar en la carpeta de la app
ImageReader.OnImageAvailableListener readerListener = iReader -> {
try (Image image = iReader.acquireLatestImage()) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
savePicture(bytes);
} catch (IOException e) {
cameraCallback.onPictureTakenFailError(e.getMessage());
}
};
mReader.setOnImageAvailableListener(readerListener, mBackgroundHandler);
final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
Log.d(TAG, "file");
//Toast.makeText(mActivity, "Saved:" + mFile, Toast.LENGTH_SHORT).show();
createCameraPreview();
cameraCallback.onPictureTakenSuccess("Saved:" + mFile);
}
};
mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
try {
session.capture(captureBuilder.build(), captureListener, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
cameraCallback.onPictureTakenFailError(e.getMessage());
}
}
private void savePicture(byte[] bytes) throws IOException {
OutputStream output = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
output = Files.newOutputStream(mFile.toPath());
} else {
output = new FileOutputStream(mFile);
}
output.write(bytes);
} finally {
if (output != null) {
output.close();
}
}
}
protected void createCameraPreview() {
try {
CameraCharacteristics cc = getCameraCharacteristics();
StreamConfigurationMap map = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
assert map != null;
mImageDimension = map.getOutputSizes(SurfaceTexture.class)[0];
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mImageDimension.getWidth(), mImageDimension.getHeight());
Surface surface = new Surface(texture);
mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mCaptureRequestBuilder.addTarget(surface);
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
//The camera is already closed
if (null == mCameraDevice) {
return;
}
// When the session is ready, we start displaying the preview.
mCameraCaptureSessions = cameraCaptureSession;
updatePreview();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Toast.makeText(mActivity, "Configuration change", Toast.LENGTH_SHORT).show();
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
protected void updatePreview() {
if (null == mCameraDevice) {
Log.e(TAG, "updatePreview error, return");
}
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
try {
mCameraCaptureSessions.setRepeatingRequest(mCaptureRequestBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public void closeCamera() {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}
private static int sensorToDeviceRotation(CameraCharacteristics cameraCharacteristics, int deviceOrientation) {
int sensorOrienatation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
deviceOrientation = ORIENTATIONS.get(deviceOrientation);
return (sensorOrienatation + deviceOrientation + 360) % 360;
}
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
int textureViewHeight) {
// Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>();
// Collect the supported resolutions that are smaller than the preview Surface
for (Size option : choices) {
if (option.getHeight() == option.getWidth() * textureViewHeight / textureViewWidth &&
option.getWidth() >= textureViewWidth &&
option.getHeight() >= textureViewHeight) {
bigEnough.add(option);
}
}
// Pick the smallest of those big enough. If there is no one big enough, pick the
// largest of those not big enough.
if (!bigEnough.isEmpty()) {
return Collections.min(bigEnough, new CompareSizeByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
}
}
private void createFolder() {
File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
mFileFolder = new File(folder, "Camera2TestApp");
if (!mFileFolder.exists()) {
mFileFolder.mkdirs();
}
}
public void resumeCamera() {
startBackgroundThread();
if (mTextureView.isAvailable()) {
setupCamera(mTextureView.getWidth(), mTextureView.getHeight());
connectCamera();
startPreview();
} else {
mTextureView.setSurfaceTextureListener(textureListener);
}
}
public void pauseCamera() {
closeCamera();
stopBackgroundThread();
}
public void destroyCamera() {
closeCamera();
}
}

View file

@ -0,0 +1,9 @@
package com.example.cameraxtestappjava.camera;
public interface SegpassCameraCallback {
void onPictureTakenSuccess(String message);
void onPictureTakenFailError(String error);
}

View file

@ -0,0 +1,6 @@
package com.example.cameraxtestappjava.camera;
public interface SegpassPermissionListener {
void onPermissionGranted();
void onPermissionDenied();
}

View file

@ -1,732 +0,0 @@
package com.example.cameraxtestappjava.segpass;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.example.cameraxtestappjava.segpass.camera.exceptions.SegpassCameraException;
import com.example.cameraxtestappjava.segpass.camera.utils.CompareSizesByArea;
import com.example.cameraxtestappjava.segpass.camera.utils.ImageSaverUtil;
import com.example.cameraxtestappjava.segpass.camera.utils.ImageEncodingCallback;
import com.example.cameraxtestappjava.segpass.camera.utils.SegpassCameraCallback;
import com.example.cameraxtestappjava.segpass.camera.utils.SegpassPermissionCallback;
import com.example.cameraxtestappjava.segpass.camera.view.AutoFitTextureView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SegpassCamera {
/**
* Tag for the {@link Log}.
*/
private static final String TAG = "SegpassCamera";
/**
* Class variabels
*/
AppCompatActivity mActivity;
SegpassPermissionCallback mPermissionListener;
SegpassCameraCallback mCameraCallback;
/**
* Conversion from screen rotation to JPEG orientation.
*/
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
public static final int REQUEST_CAMERA_PERMISSION = 200;
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
/**
* Max preview width that is guaranteed by Camera2 API
*/
private static final int MAX_PREVIEW_WIDTH = 1920;
/**
* Max preview height that is guaranteed by Camera2 API
*/
private static final int MAX_PREVIEW_HEIGHT = 1080;
/**
* Error constants
*/
private static final String ERROR_CAMERA_ACCESS = "Camera access error.";
/**
* ID of the current {@link CameraDevice}.
*/
private String mCameraId;
/**
* An {@link AutoFitTextureView} for camera preview.
*/
private final AutoFitTextureView mTextureView;
/**
* A {@link CameraCaptureSession } for camera preview.
*/
private CameraCaptureSession mCaptureSession;
/**
* A reference to the opened {@link CameraDevice}.
*/
private CameraDevice mCameraDevice;
/**
* The {@link android.util.Size} of camera preview.
*/
private Size mPreviewSize;
/**
* An additional thread for running tasks that shouldn't block the UI.
*/
private HandlerThread mBackgroundThread;
/**
* A {@link Handler} for running tasks in the background.
*/
private Handler mBackgroundHandler;
/**
* An {@link ImageReader} that handles still image capture.
*/
private ImageReader mImageReader;
/**
* This is the resulting image encoded to Base64
*/
private String mBase64Value;
/**
* {@link CaptureRequest.Builder} for the camera preview
*/
private CaptureRequest.Builder mPreviewRequestBuilder;
/**
* {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder}
*/
private CaptureRequest mPreviewRequest;
/**
* A {@link Semaphore} to prevent the app from exiting before closing the camera.
*/
private final Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
* Orientation of the camera sensor
*/
private int mSensorOrientation;
/**
* @param activity Parent activity where the camera is called from.
* @param textureView {@link AutoFitTextureView} where the camera preview is shown.
* @param listener {@link SegpassPermissionCallback} that deals with camera and storage permissions.
* @param cameraCallback {@link SegpassCameraCallback} that deals with operation results.
*/
public SegpassCamera(AppCompatActivity activity, AutoFitTextureView textureView, SegpassPermissionCallback listener, SegpassCameraCallback cameraCallback) {
mActivity = activity;
mTextureView = textureView;
mPermissionListener = listener;
mCameraCallback = cameraCallback;
}
/**
* {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
* {@link TextureView}.
*/
private final TextureView.SurfaceTextureListener mSurfaceTextureListener
= new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
openCamera(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
configureTransform(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture texture) {
// No code implementation needed for this use case.
}
};
/**
* {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
*/
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
mActivity.finish();
}
};
/**
* {@link ImageEncodingCallback} that deals with Base64 transformation.
*/
private final ImageEncodingCallback base64Callback = new ImageEncodingCallback() {
@Override
public void onImageEncodeSuccess(String base64) {
Log.d(TAG, "onBase64TransformationSuccess@base64Callback: image encoded successfully.");
mBase64Value = base64;
}
@Override
public void onImageEncodeFail(String error) {
Log.e(TAG, "onBase64TransformationSuccess@base64Callback" + error);
mBase64Value = null;
}
};
/**
* This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a
* still image is ready to be saved (or sent to be encoded to base64)
*/
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = reader ->
mBackgroundHandler.post(new ImageSaverUtil(reader.acquireNextImage(), base64Callback));
/**
* Starts a background thread and its {@link Handler}.
*/
private void startBackgroundThread() {
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
/**
* Stops the background thread and its {@link Handler}.
*/
private void stopBackgroundThread() {
mBackgroundThread.quitSafely();
try {
mBackgroundThread.join();
mBackgroundThread = null;
mBackgroundHandler = null;
} catch (InterruptedException e) {
mBackgroundThread.interrupt();
Log.d(TAG, "InterruptedException@stopBackgroundThread(): " + e.getMessage());
}
}
/**
* Starts or resume a Camera. Must be invoked after {@link Activity} super.onResume().
*/
public void resumeCamera() {
startBackgroundThread();
// When the screen is turned off and turned back on, the SurfaceTexture is already
// available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
// a camera and start preview from here (otherwise, we wait until the surface is ready in
// the SurfaceTextureListener).
if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
} else {
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
/**
* Pauses the camera (when the application leaves the activity or the app is sent to
* background. Must be invoked before {@link Activity} super.onPause().
*/
public void pauseCamera() {
closeCamera();
stopBackgroundThread();
}
/**
* Shows a {@link Toast} on the UI thread.
*
* @param text The message to show
*/
public void showToast(String text) {
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, text, Toast.LENGTH_LONG).show());
}
/**
* Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
* is at least as large as the respective texture view size, and that is at most as large as the
* respective max size, and whose aspect ratio matches with the specified value. If such size
* doesn't exist, choose the largest one that is at most as large as the respective max size,
* and whose aspect ratio matches with the specified value.
*
* @param choices The list of sizes that the camera supports for the intended output
* class
* @param textureViewWidth The width of the texture view relative to sensor coordinate
* @param textureViewHeight The height of the texture view relative to sensor coordinate
* @param maxWidth The maximum width that can be chosen
* @param maxHeight The maximum height that can be chosen
* @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
// Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>();
// Collect the supported resolutions that are smaller than the preview Surface
List<Size> notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
option.getHeight() == option.getWidth() * h / w) {
if (option.getWidth() >= textureViewWidth &&
option.getHeight() >= textureViewHeight) {
bigEnough.add(option);
} else {
notBigEnough.add(option);
}
}
}
// Pick the smallest of those big enough. If there is no one big enough, pick the
// largest of those not big enough.
if (!bigEnough.isEmpty()) {
return Collections.min(bigEnough, new CompareSizesByArea());
} else if (!notBigEnough.isEmpty()) {
return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
}
}
/**
* Sets up member variables related to camera.
*
* @param width The width of available size for camera preview
* @param height The height of available size for camera preview
*/
private void setUpCameraOutputs(int width, int height) {
CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
try {
for (String cameraId : manager.getCameraIdList()) {
CameraCharacteristics characteristics
= manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = getStreamConfigurationMap(characteristics);
if (map == null) continue;
// For still image captures, we use the largest available size.
Size largest = Collections.max(
Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
new CompareSizesByArea());
setImageReaderListeners(largest);
boolean swappedDimensions = isSwappedDimensions(characteristics);
getOptimalPreviewSize(width, height, swappedDimensions, map, largest);
setPreviewAspectRatio();
mCameraId = cameraId;
return;
}
} catch (CameraAccessException e) {
Log.e(TAG, "CameraAccessException@setUpCameraOutputs(): " + e.getMessage());
mCameraCallback.onCameraInitError(e.getMessage());
} catch (NullPointerException e) {
Log.e(TAG, "NullPointerException@setUpCameraOutputs(): " + e.getMessage());
mCameraCallback.onCameraInitError(e.getMessage());
}
}
private boolean isSwappedDimensions(CameraCharacteristics characteristics) {
// Find out if we need to swap dimension to get the preview size relative to sensor
// coordinate.
int displayRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
//noinspection ConstantConditions
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
boolean swappedDimensions = false;
switch (displayRotation) {
case Surface.ROTATION_0:
case Surface.ROTATION_180:
if (mSensorOrientation == 90 || mSensorOrientation == 270) {
swappedDimensions = true;
}
break;
case Surface.ROTATION_90:
case Surface.ROTATION_270:
if (mSensorOrientation == 0 || mSensorOrientation == 180) {
swappedDimensions = true;
}
break;
default:
Log.e(TAG, "Display rotation is invalid: " + displayRotation);
}
return swappedDimensions;
}
@Nullable
private static StreamConfigurationMap getStreamConfigurationMap(CameraCharacteristics characteristics) {
// We don't use a back facing camera.
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
// If facis is null, return
if (facing == null) return null;
if (facing == CameraMetadata.LENS_FACING_BACK) return null;
return characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
}
private void getOptimalPreviewSize(int width, int height, boolean swappedDimensions, StreamConfigurationMap map, Size largest) {
Point displaySize = new Point();
mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize);
int rotatedPreviewWidth = width;
int rotatedPreviewHeight = height;
int maxPreviewWidth = displaySize.x;
int maxPreviewHeight = displaySize.y;
if (swappedDimensions) {
rotatedPreviewWidth = height;
rotatedPreviewHeight = width;
maxPreviewWidth = displaySize.y;
maxPreviewHeight = displaySize.x;
}
if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
maxPreviewWidth = MAX_PREVIEW_WIDTH;
}
if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
maxPreviewHeight = MAX_PREVIEW_HEIGHT;
}
// Danger, W.R.! Attempting to use too large a preview size could exceed the camera
// bus' bandwidth limitation, resulting in gorgeous previews but the storage of
// garbage capture data.
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
maxPreviewHeight, largest);
}
private void setImageReaderListeners(Size largest) {
mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
ImageFormat.JPEG, /*maxImages*/2);
mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler);
}
private void setPreviewAspectRatio() {
// We fit the aspect ratio of TextureView to the size of preview we picked.
int orientation = mActivity.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
mTextureView.setAspectRatio(
mPreviewSize.getWidth(), mPreviewSize.getHeight());
} else {
mTextureView.setAspectRatio(
mPreviewSize.getHeight(), mPreviewSize.getWidth());
}
}
/**
* Opens the camera specified by {@link SegpassCamera#mCameraId}.
*/
private void openCamera(int width, int height) {
// Check permissions
if (ContextCompat.checkSelfPermission(mActivity, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
mPermissionListener.onPermissionRequest();
return;
}
setUpCameraOutputs(width, height);
configureTransform(width, height);
CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new SegpassCameraException("Time out waiting to lock camera opening.");
}
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "CameraAccessException@openCamera(): " + e.getMessage());
mCameraCallback.onCameraInitError(e.getMessage());
} catch (InterruptedException e) {
mBackgroundThread.interrupt();
Log.e(TAG, "InterruptedException@openCamera(): " + e.getMessage());
}
}
/**
* Closes the current {@link CameraDevice}.
*/
private void closeCamera() {
try {
mCameraOpenCloseLock.acquire();
if (null != mCaptureSession) {
mCaptureSession.close();
mCaptureSession = null;
}
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
if (null != mImageReader) {
mImageReader.close();
mImageReader = null;
}
} catch (InterruptedException e) {
mBackgroundThread.interrupt();
Log.e(TAG, "InterruptedException@closeCamera(): " + e.getMessage());
} finally {
mCameraOpenCloseLock.release();
}
}
/**
* Creates a new {@link CameraCaptureSession} for camera preview.
*/
private void createCameraPreviewSession() {
try {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
// We configure the size of default buffer to be the size of camera preview we want.
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
// This is the output Surface we need to start preview.
Surface surface = new Surface(texture);
// We set up a CaptureRequest.Builder with the output Surface.
mPreviewRequestBuilder
= mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
// Here, we create a CameraCaptureSession for camera preview.
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
// The camera is already closed
if (null == mCameraDevice) {
return;
}
// When the session is ready, we start displaying the preview.
mCaptureSession = cameraCaptureSession;
try {
// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();
mCaptureSession.setRepeatingRequest(mPreviewRequest,
null, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "onConfigured(): " + e.getMessage());
mCameraCallback.onCameraInitError(e.getMessage());
}
}
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
mCameraCallback.onCameraInitError("Camera capture session failed.");
}
}, null
);
} catch (CameraAccessException e) {
Log.e(TAG, "createCameraPreviewSession(): " + e.getMessage());
mCameraCallback.onCameraInitError(ERROR_CAMERA_ACCESS);
}
}
/**
* Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`.
* This method should be called after the camera preview size is determined in
* setUpCameraOutputs and also the size of `mTextureView` is fixed.
*
* @param viewWidth The width of `mTextureView`
* @param viewHeight The height of `mTextureView`
*/
private void configureTransform(int viewWidth, int viewHeight) {
if (null == mTextureView || null == mPreviewSize) {
return;
}
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
Matrix matrix = new Matrix();
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
float centerX = viewRect.centerX();
float centerY = viewRect.centerY();
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
float scale = Math.max(
(float) viewHeight / mPreviewSize.getHeight(),
(float) viewWidth / mPreviewSize.getWidth());
matrix.postScale(scale, scale, centerX, centerY);
matrix.postRotate(90f * (rotation - 2), centerX, centerY);
} else if (Surface.ROTATION_180 == rotation) {
matrix.postRotate(180, centerX, centerY);
}
mTextureView.setTransform(matrix);
}
/**
* Initiate a still image capture.
*/
public void takePicture() {
// Validate that the camera is available
if (mCameraDevice == null) {
Log.e(TAG, "cameraDevice is null");
return;
}
try {
// Get camera manager and characteristics
CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraId);
//
Size[] jpegSizes = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(ImageFormat.JPEG);
if (jpegSizes == null) throw new NullPointerException("Error taking picture");
// Default dimensions
int width = 640;
int height = 480;
if (jpegSizes.length > 0) {
width = jpegSizes[0].getWidth();
height = jpegSizes[0].getHeight();
}
// Set a new instance of ImageReader
ImageReader mCaptureImageReader = getCaptureImageReader(width, height);
// Set the output surfaces (from where the picture is taken)
List<Surface> outputSurfaces = new ArrayList<>(2);
outputSurfaces.add(mCaptureImageReader.getSurface());
outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture()));
// Create the CaptiureBuilder
final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mCaptureImageReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
// Picture orientation
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
mCaptureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
if (mBase64Value != null) {
Log.i(TAG, "onCaptureCompleted@takePicture(): sending Base64 result on callback... ");
mCameraCallback.onPictureTakenSuccess(mBase64Value);
} else {
Log.e(TAG, "onCaptureCompleted@takePicture(): error saving picture, base64 value is null.");
mCameraCallback.onPictureTakenFailError("Error saving picture...");
}
Log.d(TAG, "Recreating camera preview...");
createCameraPreviewSession();
}
};
mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
try {
session.capture(captureBuilder.build(), captureListener, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "onConfigured@takePicture(): " + e.getMessage());
mCameraCallback.onCameraInitError(ERROR_CAMERA_ACCESS);
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// No implementation needed here.
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
Log.e(TAG, "CameraAccessException@takePicture(): " + e.getMessage());
mCameraCallback.onCameraInitError(ERROR_CAMERA_ACCESS);
}
}
@NonNull
private static ImageReader getCaptureImageReader(int width, int height) {
return ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
}
private int getOrientation(int rotation) {
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
// We have to take that into account and rotate JPEG properly.
// For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
// For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
}
}

View file

@ -1,40 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.example.cameraxtestappjava.segpass.camera.interfaces.SegpassDialogListener;
public class SegpassConfirmationDialogFragment extends DialogFragment {
private final SegpassDialogListener listener;
private final String message;
public SegpassConfirmationDialogFragment(String message, SegpassDialogListener listener) {
this.listener = listener;
this.message = message;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(requireActivity())
.setMessage(message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (listener != null) {
listener.onOkClicked();
}
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if (listener != null) {
listener.onCancelClicked();
}
dismiss(); // Dismiss the dialog
})
.create();
}
}

View file

@ -1,38 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.example.cameraxtestappjava.segpass.camera.utils.SegpassErrorDialogListener;
public class SegpassErrorDialogFragment extends DialogFragment {
private final String message;
private final SegpassErrorDialogListener listener;
public SegpassErrorDialogFragment(String message, SegpassErrorDialogListener listener) {
this.message = message;
this.listener = listener;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
assert activity != null;
return new AlertDialog.Builder(activity)
.setMessage(message)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
if (listener != null) {
listener.onErrorOkClick(); // Call listener if available
}
dismiss(); // Dismiss the dialog
})
.create();
}
}

View file

@ -1,12 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.exceptions;
public class SegpassCameraException extends RuntimeException {
public SegpassCameraException(String message) {
super(message);
}
public SegpassCameraException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,6 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.interfaces;
public interface SegpassDialogListener {
void onOkClicked();
void onCancelClicked();
}

View file

@ -1,19 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
import android.util.Size;
import java.util.Comparator;
/**
* Compares two {@code Size}s based on their areas.
*/
public class CompareSizesByArea implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
// We cast here to ensure the multiplications won't overflow
return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
(long) rhs.getWidth() * rhs.getHeight());
}
}

View file

@ -1,8 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
public interface ImageEncodingCallback {
void onImageEncodeSuccess(String base64);
void onImageEncodeFail(String error);
}

View file

@ -1,33 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
import android.media.Image;
import android.util.Base64;
import java.nio.ByteBuffer;
public class ImageSaverUtil implements Runnable {
/**
* The JPEG image
*/
private final Image mImage;
private final ImageEncodingCallback mCallback;
public ImageSaverUtil(Image image, ImageEncodingCallback callback) {
mImage = image;
mCallback = callback;
}
@Override
public void run() {
try {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
mCallback.onImageEncodeSuccess(Base64.encodeToString(bytes, Base64.NO_WRAP));
} catch (Exception e) {
e.printStackTrace();
mCallback.onImageEncodeFail(e.getMessage());
}
}
}

View file

@ -1,9 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
public interface SegpassCameraCallback {
void onPictureTakenSuccess(String base64);
void onPictureTakenFailError(String error);
void onCameraInitError(String errorMessage);
}

View file

@ -1,5 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
public interface SegpassErrorDialogListener {
void onErrorOkClick();
}

View file

@ -1,7 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.utils;
public interface SegpassPermissionCallback {
void onPermissionRequest();
void onPermissionGranted();
void onPermissionDenied();
}

View file

@ -1,57 +0,0 @@
package com.example.cameraxtestappjava.segpass.camera.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.TextureView;
public class AutoFitTextureView extends TextureView {
private int mRatioWidth = 0;
private int mRatioHeight = 0;
public AutoFitTextureView(Context context) {
this(context, null);
}
public AutoFitTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
* calculated from the parameters. Note that the actual sizes of parameters don't matter, that
* is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
*
* @param width Relative horizontal size
* @param height Relative vertical size
*/
public void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
}

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraActivityOld">
<include
android:id="@+id/inCamera2"
layout="@layout/camera_autofit_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -7,4 +7,10 @@
android:layout_height="match_parent"
tools:context=".Camera2Activity">
<include
android:id="@+id/c2Camera"
layout="@layout/custom_camera2_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraActivityNew">
<include
android:id="@+id/inCamera2"
layout="@layout/camera_autofit_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -7,14 +7,46 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/goToCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
<include
android:id="@+id/c2Camera"
layout="@layout/custom_camera2_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:visibility="gone"
android:id="@+id/cvCustomCamera"
layout="@layout/custom_camera_view"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<!--<androidx.camera.view.PreviewView
android:id="@+id/pvViewfinder"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/llCameraButtons"
app:layout_constraintEnd_toEndOf="parent"
android:text="OPEN CAMERA" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llCameraButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pvViewfinder">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnTakePicture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Take Picture" />
</LinearLayout>-->
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.cameraxtestappjava.segpass.camera.view.AutoFitTextureView
android:id="@+id/tvCameraTextureView"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageButton
android:id="@+id/btn_takepicture"
android:layout_width="80dip"
android:layout_height="80dip"
android:background="?android:selectableItemBackground"
android:padding="10dip"
android:layout_gravity="bottom|center"
android:scaleType="fitCenter"
android:src="@drawable/capture_button_video" />
</FrameLayout>

View file

@ -10,10 +10,57 @@
<TextureView
android:id="@+id/tvCameraTextureView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/btn_takepicture"
android:layout_height="wrap_content"
android:layout_above="@+id/btn_takepicture"
android:layout_alignParentTop="true" />
<RelativeLayout
android:id="@+id/rl1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="#66000000"
android:minHeight="200px">
<TextView
android:id="@+id/tv2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="Camera Ids:"
android:textColor="#ffffff" />
</RelativeLayout>
<ImageButton
android:id="@+id/sw_lens"
android:layout_width="50dip"
android:layout_height="50dip"
android:layout_above="@+id/rl2"
android:layout_centerHorizontal="true"
android:background="?android:selectableItemBackground"
android:contentDescription="switch_lens"
android:padding="10dip"
android:scaleType="fitCenter"
android:src="@drawable/capture_button_video"
android:text="1X" />
<RelativeLayout
android:id="@+id/rl2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#66000000"
android:minHeight="500px">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="Camera Ids:"
android:textColor="#ffffff" />
</RelativeLayout>
<ImageButton
android:id="@+id/btn_takepicture"
android:layout_width="80dip"

View file

@ -8,7 +8,6 @@ material = "1.12.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
camerax = "1.3.3"
robolectric = "4.8.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -23,7 +22,6 @@ camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.r
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax"}
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax"}
camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax"}
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }