diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3915541..0550ef1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,9 @@ android:supportsRtl="true" android:theme="@style/Theme.CameraXTestAppJava" tools:targetApi="31"> + diff --git a/app/src/main/java/com/example/cameraxtestappjava/CameraActivityNew.java b/app/src/main/java/com/example/cameraxtestappjava/CameraActivityNew.java index 74e2e87..76ea9e5 100644 --- a/app/src/main/java/com/example/cameraxtestappjava/CameraActivityNew.java +++ b/app/src/main/java/com/example/cameraxtestappjava/CameraActivityNew.java @@ -76,7 +76,7 @@ public class CameraActivityNew extends AppCompatActivity implements ActivityComp } private void setUpListeners() { - binding.inCamera2.btnTakepicture.setOnClickListener(v -> mSegpassCamera.takePicture()); + binding.inCamera2.btnTakePhoto.setOnClickListener(v -> mSegpassCamera.takePicture()); } /** diff --git a/app/src/main/java/com/example/cameraxtestappjava/MainActivity.java b/app/src/main/java/com/example/cameraxtestappjava/MainActivity.java index 1c4799c..f4bfa88 100644 --- a/app/src/main/java/com/example/cameraxtestappjava/MainActivity.java +++ b/app/src/main/java/com/example/cameraxtestappjava/MainActivity.java @@ -39,7 +39,7 @@ public class MainActivity extends AppCompatActivity { private void setListeners() { binding.goToCamera.setOnClickListener(v -> { - Intent intent = new Intent(this, CameraActivityNew.class); + Intent intent = new Intent(this, SegpassCameraActivity.class); startActivity(intent); }); } diff --git a/app/src/main/java/com/example/cameraxtestappjava/SegpassCameraActivity.java b/app/src/main/java/com/example/cameraxtestappjava/SegpassCameraActivity.java new file mode 100644 index 0000000..5ca8e9f --- /dev/null +++ b/app/src/main/java/com/example/cameraxtestappjava/SegpassCameraActivity.java @@ -0,0 +1,119 @@ +package com.example.cameraxtestappjava; + +import static com.example.cameraxtestappjava.segpass.SegpassCamera.REQUEST_CAMERA_PERMISSION; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +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 com.example.cameraxtestappjava.databinding.ActivitySegpassCameraBinding; +import com.example.cameraxtestappjava.segpass.camera.utils.SegpassCameraCallback; +import com.example.cameraxtestappjava.segpass.camera.utils.SegpassPermissionCallback; +import com.example.cameraxtestappjava.segpass.camera.view.SegpassCameraLayout; + +public class SegpassCameraActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback, SegpassCameraCallback, SegpassPermissionCallback { + + ActivitySegpassCameraBinding binding; + SegpassCameraLayout cameraLayout; + + @SuppressLint("SourceLockedOrientationActivity") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + + binding = ActivitySegpassCameraBinding.inflate(getLayoutInflater()); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + setContentView(binding.getRoot()); + + setCamera(); + + 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; + }); + } + + private void setCamera() { + cameraLayout = binding.sclCameraLayout; + cameraLayout.initSegpassCamera(this, this, this); + } + + @Override + public void onResume() { + super.onResume(); + cameraLayout.resumeCamera(); + } + + @Override + public void onPause() { + cameraLayout.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); + } + } + + /** + * Camera callback + */ + + @Override + public void onPictureTakenSuccess(String base64) { + cameraLayout.showToast("Base64 generated."); + } + + @Override + public void onPictureTakenFailError(String error) { + cameraLayout.showToast(error); + } + + /** + * Camera state callback (just to validate if there was an error while initializing the camera) + */ + + @Override + public void onCameraInitError(String errorMessage) { + cameraLayout.showToast(errorMessage); + } + + /** + * Permissions callbck + */ + + @Override + public void onPermissionRequest() { + requestPermissions(new String[]{android.Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CAMERA_PERMISSION); + } + + @Override + public void onPermissionGranted() { + // Do nothing, use camera. + } + + @Override + public void onPermissionDenied() { + cameraLayout.showToast("No permissions granted, closing camera."); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/cameraxtestappjava/segpass/SegpassCamera.java b/app/src/main/java/com/example/cameraxtestappjava/segpass/SegpassCamera.java index ff3e450..93f1dff 100644 --- a/app/src/main/java/com/example/cameraxtestappjava/segpass/SegpassCamera.java +++ b/app/src/main/java/com/example/cameraxtestappjava/segpass/SegpassCamera.java @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -57,7 +58,7 @@ public class SegpassCamera { private static final String TAG = "SegpassCamera"; /** - * Class variabels + * Class variables */ AppCompatActivity mActivity; SegpassPermissionCallback mPermissionListener; @@ -403,6 +404,13 @@ public class SegpassCamera { } } + /** + * Validates whether the camera's current rotation is different from the output shown in the + * preview {@link AutoFitTextureView} + * + * @param characteristics Camera characteristics + * @return boolean value for display rotation + */ private boolean isSwappedDimensions(CameraCharacteristics characteristics) { // Find out if we need to swap dimension to get the preview size relative to sensor // coordinate. @@ -429,12 +437,18 @@ public class SegpassCamera { return swappedDimensions; } + /** + * Obtains configuration map to be applied to the preview surface + * + * @param characteristics Camera characteristics + * @return the configuration map for the {@link AutoFitTextureView} + */ @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 is null, return if (facing == null) return null; if (facing == CameraMetadata.LENS_FACING_BACK) return null; @@ -442,6 +456,15 @@ public class SegpassCamera { CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); } + /** + * Obtains optimal preview size to be shown in {@link AutoFitTextureView} + * + * @param width Current {@link AutoFitTextureView} width + * @param height Current {@link AutoFitTextureView} height + * @param swappedDimensions Whether the preview surface is rotate in relation to the previwe + * @param map Configuration map to be applies to the preview surface + * @param largest Largest display (i.e. image) {@link Size} supported by the preview. + */ private void getOptimalPreviewSize(int width, int height, boolean swappedDimensions, StreamConfigurationMap map, Size largest) { Point displaySize = new Point(); mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize); @@ -473,6 +496,11 @@ public class SegpassCamera { maxPreviewHeight, largest); } + /** + * Set image reader listeners + * + * @param largest Largest display (i.e. image) {@link Size} supported by the preview. + */ private void setImageReaderListeners(Size largest) { mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /*maxImages*/2); @@ -480,6 +508,9 @@ public class SegpassCamera { mOnImageAvailableListener, mBackgroundHandler); } + /** + * Sets the preview surface aspect ratio considering the screen orientation/ + */ private void setPreviewAspectRatio() { // We fit the aspect ratio of TextureView to the size of preview we picked. int orientation = mActivity.getResources().getConfiguration().orientation; @@ -647,8 +678,8 @@ public class SegpassCamera { 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); + // Obtains list of image size supported by the selected camera sensor. + Size[] jpegSizes = Objects.requireNonNull(characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)).getOutputSizes(ImageFormat.JPEG); if (jpegSizes == null) throw new NullPointerException("Error taking picture"); // Default dimensions @@ -677,6 +708,7 @@ public class SegpassCamera { int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); + // Listens when the image is finally taken. mCaptureImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() { @Override @@ -694,6 +726,7 @@ public class SegpassCamera { } }; + // Initialized the session to listen to a picture capture event. mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { @@ -717,11 +750,24 @@ public class SegpassCamera { } + /** + * Gets the ImageReader for capturing a still picture. + * + * @param width Supported image width by surface and sensor. + * @param height Supported image height by surface and sensor. + * @return {@link ImageReader} object for the preview surface. + */ @NonNull private static ImageReader getCaptureImageReader(int width, int height) { return ImageReader.newInstance(width, height, ImageFormat.JPEG, 1); } + /** + * Obtains orientation that will be applied to the captured image. + * + * @param rotation Rotation angle. + * @return Orientation value. + */ 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. diff --git a/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/utils/ImageSaverUtil.java b/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/utils/ImageSaverUtil.java index 2713aa3..747e5cc 100644 --- a/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/utils/ImageSaverUtil.java +++ b/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/utils/ImageSaverUtil.java @@ -1,16 +1,29 @@ package com.example.cameraxtestappjava.segpass.camera.utils; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.media.Image; import android.util.Base64; +import android.util.Log; +import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; public class ImageSaverUtil implements Runnable { + /** + * Tag for the {@link Log} + */ + private static final String TAG = "ImageSaverUtil"; + /** * The JPEG image */ private final Image mImage; + + /** + * Callback to return encoded image value in {@link Base64} + */ private final ImageEncodingCallback mCallback; public ImageSaverUtil(Image image, ImageEncodingCallback callback) { @@ -21,12 +34,35 @@ public class ImageSaverUtil implements Runnable { @Override public void run() { try { + + // Get image as byte data buffer ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); - mCallback.onImageEncodeSuccess(Base64.encodeToString(bytes, Base64.NO_WRAP)); + + // Convert byte data to a Bitmap + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + + // Create a ByteArrayOutputStream to capture the compressed image data + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + // Compress the bitmap with JPEG format and desired quality (25% will do for this use case) + bitmap.compress(Bitmap.CompressFormat.JPEG, 25, outputStream); + + // Get the compressed image data as a byte array + byte[] compressedImageData = outputStream.toByteArray(); + + // Encode the compressed data to Base64 string + String base64String = Base64.encodeToString(compressedImageData, Base64.NO_WRAP); + + // Close the stream (optional, as ByteArrayOutputStream should close automatically) + outputStream.close(); + + // Return Base64 string on callback + mCallback.onImageEncodeSuccess(base64String); + } catch (Exception e) { - e.printStackTrace(); + Log.e(TAG, "Error encoding photo: " + e.getMessage()); mCallback.onImageEncodeFail(e.getMessage()); } } diff --git a/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/view/SegpassCameraLayout.java b/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/view/SegpassCameraLayout.java new file mode 100644 index 0000000..8018f30 --- /dev/null +++ b/app/src/main/java/com/example/cameraxtestappjava/segpass/camera/view/SegpassCameraLayout.java @@ -0,0 +1,80 @@ +package com.example.cameraxtestappjava.segpass.camera.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.example.cameraxtestappjava.R; +import com.example.cameraxtestappjava.segpass.SegpassCamera; +import com.example.cameraxtestappjava.segpass.camera.utils.SegpassCameraCallback; +import com.example.cameraxtestappjava.segpass.camera.utils.SegpassPermissionCallback; + +public class SegpassCameraLayout extends FrameLayout { + private AppCompatActivity mActivity; + private AutoFitTextureView mTextureView; + private SegpassPermissionCallback mPermissionListener; + private SegpassCameraCallback mCameraCallback; + private SegpassCamera mCamera; + private ImageButton mTakePicture; + + private final View rootView; + + public SegpassCameraLayout(@NonNull Context context) { + super(context); + rootView = inflate(context, R.layout.segpass_camera_view, this); + } + + public SegpassCameraLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + rootView = inflate(context, R.layout.segpass_camera_view, this); + } + + public SegpassCameraLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + rootView = inflate(context, R.layout.segpass_camera_view, this); + } + + public SegpassCameraLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + rootView = inflate(context, R.layout.segpass_camera_view, this); + } + + /** + * @param activity Parent activity where the camera is called from. + * @param listener {@link SegpassPermissionCallback} that deals with camera and storage permissions. + * @param cameraCallback {@link SegpassCameraCallback} that deals with operation results. + */ + public void initSegpassCamera(AppCompatActivity activity, SegpassPermissionCallback listener, SegpassCameraCallback cameraCallback) { + mActivity = activity; + mPermissionListener = listener; + mCameraCallback = cameraCallback; + mTextureView = rootView.findViewById(R.id.tvCameraTextureView); + mCamera = new SegpassCamera(mActivity, mTextureView, mPermissionListener, mCameraCallback); + mCamera.resumeCamera(); + setUpListeners(); + } + + private void setUpListeners() { + mTakePicture = rootView.findViewById(R.id.btnTakePhoto); + mTakePicture.setOnClickListener(v -> mCamera.takePicture()); + } + + public void resumeCamera() { + mCamera.resumeCamera(); + } + + public void pauseCamera() { + mCamera.pauseCamera(); + } + + public void showToast(String message) { + mCamera.showToast(message); + } +} diff --git a/app/src/main/res/drawable/capture_button_video.xml b/app/src/main/res/drawable/capture_picture_button.xml similarity index 100% rename from app/src/main/res/drawable/capture_button_video.xml rename to app/src/main/res/drawable/capture_picture_button.xml diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml index 07fcc4b..d85225c 100644 --- a/app/src/main/res/layout/activity_camera.xml +++ b/app/src/main/res/layout/activity_camera.xml @@ -9,7 +9,7 @@ diff --git a/app/src/main/res/layout/activity_camera_new.xml b/app/src/main/res/layout/activity_camera_new.xml index a304607..050b3df 100644 --- a/app/src/main/res/layout/activity_camera_new.xml +++ b/app/src/main/res/layout/activity_camera_new.xml @@ -9,7 +9,7 @@ diff --git a/app/src/main/res/layout/activity_segpass_camera.xml b/app/src/main/res/layout/activity_segpass_camera.xml new file mode 100644 index 0000000..f40f564 --- /dev/null +++ b/app/src/main/res/layout/activity_segpass_camera.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_camera2_view.xml b/app/src/main/res/layout/custom_camera2_view.xml index 5f189cd..c28d3cf 100644 --- a/app/src/main/res/layout/custom_camera2_view.xml +++ b/app/src/main/res/layout/custom_camera2_view.xml @@ -11,11 +11,11 @@ android:id="@+id/tvCameraTextureView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_above="@id/btn_takepicture" + android:layout_above="@id/btnTakePhoto" android:layout_alignParentTop="true" /> + android:src="@drawable/capture_picture_button" /> diff --git a/app/src/main/res/layout/camera_autofit_view.xml b/app/src/main/res/layout/segpass_camera_view.xml similarity index 77% rename from app/src/main/res/layout/camera_autofit_view.xml rename to app/src/main/res/layout/segpass_camera_view.xml index a3d0584..f8387b7 100644 --- a/app/src/main/res/layout/camera_autofit_view.xml +++ b/app/src/main/res/layout/segpass_camera_view.xml @@ -1,11 +1,9 @@ + android:orientation="vertical"> + android:src="@drawable/capture_picture_button" />