Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
fbe834b8e2 | |||
986cf8cf9a | |||
35a3f6cba3 | |||
1ad060a816 | |||
5f8cfd5a1f | |||
ebdff6b407 | |||
01bc596284 | |||
623c1bf95f | |||
fcde6328a5 | |||
09670b4a33 | |||
51bd224ce5 | |||
2f449a55c8 | |||
5aeeee0b44 | |||
11d14f2517 | |||
6fdb9f4f7b | |||
43415be913 | |||
84d1dcf058 | |||
711c362486 | |||
f254d77dd4 | |||
807a8619a8 | |||
022542cc72 | |||
674664e059 | |||
ee00263034 |
28 changed files with 1211 additions and 777 deletions
|
@ -48,4 +48,5 @@ dependencies {
|
|||
testImplementation libs.junit
|
||||
androidTestImplementation libs.ext.junit
|
||||
androidTestImplementation libs.espresso.core
|
||||
testImplementation libs.robolectric
|
||||
}
|
|
@ -7,7 +7,8 @@
|
|||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -19,6 +20,12 @@
|
|||
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" />
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,24 +1,15 @@
|
|||
package com.example.cameraxtestappjava;
|
||||
package com.example.cameraxtestappjava;
|
||||
|
||||
import static com.example.cameraxtestappjava.camera.CustomCamera2.REQUEST_CAMERA_PERMISSION;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
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.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.example.cameraxtestappjava.camera.CustomCamera2;
|
||||
import com.example.cameraxtestappjava.camera.CustomCameraCallback;
|
||||
import com.example.cameraxtestappjava.databinding.ActivityMainBinding;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
@ -28,7 +19,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
// Camera2
|
||||
private static final String TAG = "MainActivity";
|
||||
CustomCamera2 mCustomCamera2;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -38,7 +28,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setUpCamera2();
|
||||
setListeners();
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
@ -47,61 +37,17 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
mCustomCamera2.init(); // Do not skip this!!!
|
||||
|
||||
mTakePictureButton.setOnClickListener(v -> mCustomCamera2.takePicture(new CustomCameraCallback() {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
private void setListeners() {
|
||||
binding.goToCamera.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, CameraActivityNew.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
Log.d(TAG, "onResume");
|
||||
mCustomCamera2.resumeCamera();
|
||||
setListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.d(TAG, "onPause");
|
||||
mCustomCamera2.pauseCamera();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Log.d(TAG, "onPause");
|
||||
mCustomCamera2.destroyCamera();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package com.example.cameraxtestappjava.camera;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class CustomCamera {
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
ImageCapture imageCapture;
|
||||
ImageCapture.OutputFileOptions outputFileOptions;
|
||||
ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
|
||||
ContentValues contentValues;
|
||||
String fileName;
|
||||
Uri tempFileUri;
|
||||
|
||||
Context mContext;
|
||||
|
||||
PreviewView mPreviewView;
|
||||
|
||||
|
||||
public CustomCamera(Context context, PreviewView previewView) {
|
||||
mContext = context;
|
||||
mPreviewView = previewView;
|
||||
}
|
||||
|
||||
public void setUpCamera() {
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(mContext);
|
||||
cameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
mCameraProvider = cameraProviderFuture.get();
|
||||
startCustomCamera();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}, ContextCompat.getMainExecutor(mContext)); // por parametro
|
||||
}
|
||||
|
||||
private void startCustomCamera() {
|
||||
|
||||
CameraSelector cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA;
|
||||
Preview preview = new Preview.Builder().build();
|
||||
preview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
|
||||
|
||||
imageCapture = new ImageCapture.Builder().build();
|
||||
|
||||
try {
|
||||
mCameraProvider.unbindAll();
|
||||
mCameraProvider.bindToLifecycle((LifecycleOwner) mContext, cameraSelector, preview, imageCapture);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ImageCapture.OutputFileOptions getOutputFileOptions(String name) {
|
||||
contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
|
||||
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraXTestApp");
|
||||
}
|
||||
|
||||
return new ImageCapture.OutputFileOptions.Builder(
|
||||
mContext.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
|
||||
).build();
|
||||
}
|
||||
|
||||
public void capturePhoto(CustomCameraCallback callback) {
|
||||
if (imageCapture == null) return;
|
||||
|
||||
fileName = System.currentTimeMillis() + "";
|
||||
outputFileOptions = getOutputFileOptions(fileName);
|
||||
|
||||
imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(mContext), new ImageCapture.OnImageSavedCallback() {
|
||||
@Override
|
||||
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
|
||||
tempFileUri = outputFileResults.getSavedUri();
|
||||
callback.onPictureTakenSuccess("Image saved! " + tempFileUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull ImageCaptureException exception) {
|
||||
callback.onPictureTakenFailError("Image Not Saved " + exception.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -1,503 +0,0 @@
|
|||
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 CustomCamera2 {
|
||||
private static final String TAG = "SegpassCamera";
|
||||
|
||||
Context mContext;
|
||||
AppCompatActivity mActivity;
|
||||
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 CustomCamera2(Context context, TextureView textureView) {
|
||||
mContext = context;
|
||||
mTextureView = textureView;
|
||||
}
|
||||
|
||||
public CustomCamera2(AppCompatActivity activity, TextureView textureView) {
|
||||
mActivity = activity;
|
||||
mTextureView = textureView;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
closeCamera();
|
||||
setupCamera(width, height);
|
||||
connectCamera();
|
||||
startPreview();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
|
||||
//Log.d(TAG, "Texture Updated!");
|
||||
}
|
||||
};
|
||||
|
||||
// Camera Device State Callback
|
||||
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
|
||||
@Override
|
||||
public void onOpened(@NonNull CameraDevice camera) {
|
||||
//This is called when the camera is open
|
||||
Log.e(TAG, "onOpened");
|
||||
mCameraDevice = camera; // ???????
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(@NonNull CameraDevice camera) {
|
||||
mCameraDevice.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull CameraDevice camera, int error) {
|
||||
mCameraDevice.close();
|
||||
mCameraDevice = null;
|
||||
}
|
||||
};
|
||||
|
||||
//Background Thread
|
||||
public void startBackgroundThread() {
|
||||
mBackgroundThread = new HandlerThread("Camera Background");
|
||||
mBackgroundThread.start();
|
||||
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
|
||||
}
|
||||
|
||||
public void stopBackgroundThread() {
|
||||
mBackgroundThread.quitSafely();
|
||||
try {
|
||||
mBackgroundThread.join();
|
||||
mBackgroundThread = null;
|
||||
mBackgroundHandler = null;
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setupCamera(int width, int height) {
|
||||
Log.d(TAG, "ENTRO ACA " + width + " " + height);
|
||||
// Shenanigans for the TextureView
|
||||
|
||||
mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
|
||||
|
||||
try {
|
||||
mCameraIds = mCameraManager.getCameraIdList();
|
||||
Log.d(TAG, "eeeeeee " + mCameraIds.length);
|
||||
for (String cameraId : mCameraManager.getCameraIdList()) {
|
||||
//tv.append(cameraId + "\n");
|
||||
}
|
||||
} catch (CameraAccessException e) {
|
||||
//tv.append("ERROR: CAMERA NOT FOUND\n");
|
||||
}
|
||||
|
||||
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);
|
||||
//mVideoSize = chooseOptimalSize(map.getOutputSizes(MediaRecorder.class), rotatedWidth, rotatedHeight);
|
||||
|
||||
} catch (CameraAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
//Connect Camera
|
||||
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) {
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Start Preview
|
||||
public void startPreview() {
|
||||
SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
|
||||
assert surfaceTexture != null;
|
||||
surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
|
||||
Surface previewSurface = new Surface(surfaceTexture);
|
||||
|
||||
try {
|
||||
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, "DAMN");
|
||||
}
|
||||
}
|
||||
|
||||
//Take Picture
|
||||
public void takePicture(CustomCameraCallback 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 (null != mCameraDevice) {
|
||||
mCameraDevice.close();
|
||||
mCameraDevice = null;
|
||||
}
|
||||
if (null != mImageReader) {
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package com.example.cameraxtestappjava.camera;
|
||||
|
||||
public interface CustomCameraCallback {
|
||||
|
||||
void onPictureTakenSuccess(String message);
|
||||
void onPictureTakenFailError(String error);
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,732 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.cameraxtestappjava.segpass.camera.interfaces;
|
||||
|
||||
public interface SegpassDialogListener {
|
||||
void onOkClicked();
|
||||
void onCancelClicked();
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.example.cameraxtestappjava.segpass.camera.utils;
|
||||
|
||||
public interface ImageEncodingCallback {
|
||||
void onImageEncodeSuccess(String base64);
|
||||
|
||||
void onImageEncodeFail(String error);
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.example.cameraxtestappjava.segpass.camera.utils;
|
||||
|
||||
public interface SegpassCameraCallback {
|
||||
|
||||
void onPictureTakenSuccess(String base64);
|
||||
void onPictureTakenFailError(String error);
|
||||
void onCameraInitError(String errorMessage);
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.example.cameraxtestappjava.segpass.camera.utils;
|
||||
|
||||
public interface SegpassErrorDialogListener {
|
||||
void onErrorOkClick();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.example.cameraxtestappjava.segpass.camera.utils;
|
||||
|
||||
public interface SegpassPermissionCallback {
|
||||
void onPermissionRequest();
|
||||
void onPermissionGranted();
|
||||
void onPermissionDenied();
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
16
app/src/main/res/layout/activity_camera.xml
Normal file
16
app/src/main/res/layout/activity_camera.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?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>
|
|
@ -7,10 +7,4 @@
|
|||
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>
|
16
app/src/main/res/layout/activity_camera_new.xml
Normal file
16
app/src/main/res/layout/activity_camera_new.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?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>
|
|
@ -7,46 +7,14 @@
|
|||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<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"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llCameraButtons"
|
||||
android:layout_width="match_parent"
|
||||
<Button
|
||||
android:id="@+id/goToCamera"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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>-->
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="OPEN CAMERA" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
26
app/src/main/res/layout/camera_autofit_view.xml
Normal file
26
app/src/main/res/layout/camera_autofit_view.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?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>
|
||||
|
|
@ -10,57 +10,10 @@
|
|||
<TextureView
|
||||
android:id="@+id/tvCameraTextureView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/btn_takepicture"
|
||||
android:layout_height="match_parent"
|
||||
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"
|
||||
|
|
|
@ -8,6 +8,7 @@ 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" }
|
||||
|
@ -22,6 +23,7 @@ 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" }
|
||||
|
|
Loading…
Reference in a new issue