From 06fd1374cf3e6e10fac6e012b9291756da2adc2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Mon, 6 Apr 2020 18:05:44 -0400
Subject: [PATCH] CameraActivity: make saving picture async, don't reencode

Change-Id: I11a5c6ce5ccdc16ae44cb768c61df75d3b6e0002
---
 .../java/cx/ring/tv/camera/CameraPreview.java | 36 ++++----
 .../ring/tv/camera/CustomCameraActivity.java  | 89 ++++++++++---------
 .../conversation/TvConversationFragment.java  |  4 +-
 .../app/src/main/res/layout/camerapicker.xml  | 19 ++--
 4 files changed, 75 insertions(+), 73 deletions(-)

diff --git a/ring-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.java b/ring-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.java
index 1191fa32e..27f1a15b9 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/camera/CameraPreview.java
@@ -24,21 +24,31 @@ import android.hardware.Camera;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 
+import androidx.annotation.NonNull;
+
 import java.io.IOException;
 
-public class CameraPreview extends SurfaceView implements
-        SurfaceHolder.Callback {
-    private SurfaceHolder mSurfaceHolder;
+public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
     private Camera mCamera;
 
     // Constructor that obtains context and camera
     @SuppressWarnings("deprecation")
-    public CameraPreview(Context context, Camera camera) {
+    public CameraPreview(Context context, @NonNull Camera camera) {
         super(context);
-        this.mCamera = camera;
-        this.mSurfaceHolder = this.getHolder();
-        this.mSurfaceHolder.addCallback(this);
-        this.mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+        mCamera = camera;
+        getHolder().addCallback(this);
+    }
+
+    public void stop()  {
+        if (mCamera == null)
+            return;
+        try {
+            mCamera.stopPreview();
+        }  catch (Exception e) {
+            // intentionally left blank
+        }
+        mCamera.release();
+        mCamera = null;
     }
 
     @Override
@@ -48,22 +58,18 @@ public class CameraPreview extends SurfaceView implements
         try {
             mCamera.setPreviewDisplay(surfaceHolder);
             mCamera.startPreview();
-        } catch (IOException e) {
+        } catch (Exception e) {
             // left blank for now
         }
     }
 
     @Override
     public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
-        if (mCamera == null)
-            return;
-        mCamera.stopPreview();
-        mCamera.release();
+        stop();
     }
 
     @Override
-    public void surfaceChanged(SurfaceHolder surfaceHolder, int format,
-            int width, int height) {
+    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
         if (mCamera == null)
             return;
         try {
diff --git a/ring-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.java b/ring-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.java
index 02a0da152..d647c4306 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/camera/CustomCameraActivity.java
@@ -25,17 +25,15 @@ package cx.ring.tv.camera;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.hardware.Camera;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraManager;
 import android.media.CamcorderProfile;
 import android.media.MediaRecorder;
-import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.MediaStore;
+import android.util.Log;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.Toast;
@@ -53,14 +51,18 @@ import butterknife.OnClick;
 import cx.ring.R;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
+import io.reactivex.Single;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
 
 public class CustomCameraActivity extends Activity {
-    public static final String TYPE_IMAGE = "image/png";
+    private static final String TAG = "CustomCameraActivity";
+    public static final String TYPE_IMAGE = "image/jpeg";
     public static final String TYPE_VIDEO = "video";
 
-    int cameraFront = -1;
-    int cameraBack = -1;
-    int currentCamera = 0;
+    private int cameraFront = -1;
+    private int cameraBack = -1;
+    private int currentCamera = 0;
 
     private MediaRecorder recorder;
     private boolean mRecording = false;
@@ -75,53 +77,52 @@ public class CustomCameraActivity extends Activity {
 
     private Camera mCamera;
     private CameraPreview mCameraPreview;
-    private final Camera.PictureCallback mPicture = (input, camera) -> {
-        if (mRecording) {
-            releaseMediaRecorder();
-        }
-        Intent intent = new Intent();
-        int result = RESULT_OK;
-        Bitmap photo = BitmapFactory.decodeByteArray(input, 0, input.length);
-        try {
-            File tempFile = AndroidFileUtils.createImageFile(this);
-            try (OutputStream outStream = new FileOutputStream(tempFile)) {
-                photo.compress(Bitmap.CompressFormat.PNG, 100, outStream);
-                outStream.flush();
+    private final Camera.PictureCallback mPicture = (input, camera) -> Single.fromCallable(() ->  {
+            mCameraPreview.stop();
+            File file = AndroidFileUtils.createImageFile(this);
+            try (OutputStream out = new FileOutputStream(file)) {
+                out.write(input);
+                out.flush();
             }
-            intent.putExtra(MediaStore.EXTRA_OUTPUT, ContentUriHandler.getUriForFile(this, ContentUriHandler.AUTHORITY_FILES, tempFile));
-            intent.setType(TYPE_IMAGE);
-        } catch (IOException e) {
-            e.printStackTrace();
-            result = RESULT_CANCELED;
-        }
-        if (getParent() == null) {
-            setResult(result, intent);
-        } else {
-            getParent().setResult(result, intent);
-        }
-        finish();
-    };
+            return ContentUriHandler.getUriForFile(this, ContentUriHandler.AUTHORITY_FILES, file);
+        })
+        .subscribeOn(Schedulers.io())
+        .observeOn(AndroidSchedulers.mainThread())
+        .subscribe(uri -> {
+                setResult(RESULT_OK, new Intent()
+                        .putExtra(MediaStore.EXTRA_OUTPUT, uri)
+                        .setType(TYPE_IMAGE));
+                finish();
+            }, e -> {
+                Log.e(TAG, "Error saving picture", e);
+                setResult(RESULT_CANCELED);
+                finish();
+            });
 
     @OnClick(R.id.button_picture)
     public void takePicture() {
+        if (mRecording)
+            releaseMediaRecorder();
         if (mCamera != null) {
-            mCamera.takePicture(null, null, mPicture);
+            mButtonPicture.setEnabled(false);
+            mButtonVideo.setVisibility(View.GONE);
+            try {
+                mCamera.takePicture(null, null, mPicture);
+            } catch (Exception e) {
+                Log.w(TAG, "Error taking picture", e);
+            }
         }
     }
 
     @OnClick(R.id.button_video)
     public void takeVideo() {
-        if (mRecording){
+        if (mRecording) {
             releaseMediaRecorder();
-            Intent intent = new Intent();
-            Uri uri = ContentUriHandler.getUriForFile(this, ContentUriHandler.AUTHORITY_FILES, mVideoFile);
-            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
-            intent.setType(TYPE_VIDEO);
-            if (getParent() == null) {
-                setResult(RESULT_OK, intent);
-            } else {
-                getParent().setResult(RESULT_OK, intent);
-            }
+            mCameraPreview.stop();
+            Intent intent = new Intent()
+                    .putExtra(MediaStore.EXTRA_OUTPUT, ContentUriHandler.getUriForFile(this, ContentUriHandler.AUTHORITY_FILES, mVideoFile))
+                    .setType(TYPE_VIDEO);
+            setResult(RESULT_OK, intent);
             finish();
             mButtonVideo.setImageResource(R.drawable.baseline_videocam_24);
             return;
@@ -155,7 +156,7 @@ public class CustomCameraActivity extends Activity {
         }
         mCameraPreview = new CameraPreview(this, mCamera);
         FrameLayout preview = findViewById(R.id.camera_preview);
-        preview.addView(mCameraPreview);
+        preview.addView(mCameraPreview, 0);
 
         if (mActionVideo) {
             mButtonVideo.setVisibility(View.VISIBLE);
diff --git a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
index 81f271dd7..4e8eb6141 100644
--- a/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/tv/conversation/TvConversationFragment.java
@@ -187,8 +187,8 @@ public class TvConversationFragment extends BaseSupportFragment<TvConversationPr
 
         ImageButton video = view.findViewById(R.id.button_video);
         video.setOnClickListener(v -> {
-            Intent intent = new Intent(getActivity(), CustomCameraActivity.class);
-            intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
+            Intent intent = new Intent(getActivity(), CustomCameraActivity.class)
+                    .setAction(MediaStore.ACTION_VIDEO_CAPTURE);
             startActivityForResult(intent, REQUEST_CODE_PHOTO);
         });
 
diff --git a/ring-android/app/src/main/res/layout/camerapicker.xml b/ring-android/app/src/main/res/layout/camerapicker.xml
index eb79dfb2a..6313c3309 100644
--- a/ring-android/app/src/main/res/layout/camerapicker.xml
+++ b/ring-android/app/src/main/res/layout/camerapicker.xml
@@ -1,19 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/camera_preview"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <FrameLayout
-        android:id="@+id/camera_preview"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
     <LinearLayout
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_alignParentBottom="true"
-        android:gravity="center"
+        android:layout_gravity="bottom|center_horizontal"
         android:orientation="horizontal">
 
         <com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -33,12 +28,12 @@
             android:layout_height="wrap_content"
             android:contentDescription="@string/action_call_decline"
             android:padding="16dp"
+            android:visibility="gone"
             app:backgroundTint="@color/green_500"
             app:rippleColor="@android:color/white"
             app:srcCompat="@drawable/baseline_videocam_24"
-            app:useCompatPadding="true"
-            android:visibility="gone" />
+            app:useCompatPadding="true" />
 
     </LinearLayout>
 
-</RelativeLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
-- 
GitLab