call: use TextureView for camera preview

Change-Id: I764078009b604a012c73e8eeff2f18a77b955d04
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
index 797b8cf..cc1e54e 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
@@ -20,6 +20,7 @@
  */
 package cx.ring.fragments;
 
+import android.app.Activity;
 import android.app.PendingIntent;
 import android.app.PictureInPictureParams;
 import android.app.RemoteAction;
@@ -27,8 +28,11 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.graphics.Matrix;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
 import android.graphics.drawable.Icon;
 import android.os.Build;
 import android.os.Bundle;
@@ -44,10 +48,13 @@
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.Surface;
 import android.view.SurfaceHolder;
+import android.view.TextureView;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
 import com.rodolfonavalon.shaperipplelibrary.model.Circle;
@@ -100,6 +107,7 @@
     private boolean restartVideo = false;
     private PowerManager.WakeLock mScreenWakeLock;
     private int mCurrentOrientation = Configuration.ORIENTATION_UNDEFINED;
+    private int mPreviewWidth = 720, mPreviewHeight = 1280;
 
     private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
 
@@ -241,6 +249,29 @@
         return binding.getRoot();
     }
 
+
+    private TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() {
+        @Override
+        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+            configureTransform(width, height);
+            presenter.previewVideoSurfaceCreated(binding.previewSurface);
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+            configureTransform(width, height);
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+            presenter.previewVideoSurfaceDestroyed();
+            return true;
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
+    };
+
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         setHasOptionsMenu(true);
@@ -271,31 +302,14 @@
                 presenter.videoSurfaceDestroyed();
             }
         });
-        view.addOnLayoutChangeListener((parent, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> presenter.layoutChanged());
         view.setOnSystemUiVisibilityChangeListener(visibility -> {
             boolean ui = (visibility & (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN)) == 0;
             presenter.uiVisibilityChanged(ui);
         });
 
-        binding.previewSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
-            @Override
-            public void surfaceCreated(SurfaceHolder holder) {
-                presenter.previewVideoSurfaceCreated(holder);
-            }
-
-            @Override
-            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-
-            }
-
-            @Override
-            public void surfaceDestroyed(SurfaceHolder holder) {
-                presenter.previewVideoSurfaceDestroyed();
-            }
-        });
-        binding.previewSurface.setZOrderMediaOverlay(true);
         binding.shapeRipple.setRippleShape(new Circle());
         binding.callSpeakerBtn.setChecked(presenter.isSpeakerphoneOn());
+        binding.previewSurface.setSurfaceTextureListener(listener);
 
         binding.dialpadEditText.addTextChangedListener(new TextWatcher() {
             @Override
@@ -389,8 +403,9 @@
 
     @Override
     public void displayVideoSurface(final boolean display) {
+        Log.w(TAG, "displayVideoSurface " + display);
         binding.videoSurface.setVisibility(display ? View.VISIBLE : View.GONE);
-        binding.previewSurface.setVisibility(display ? View.VISIBLE : View.GONE);
+        //binding.previewSurface.setVisibility(display ? View.VISIBLE : View.GONE);
         updateMenu();
     }
 
@@ -398,10 +413,10 @@
     public void displayPreviewSurface(final boolean display) {
         if (display) {
             binding.videoSurface.setZOrderOnTop(false);
-            binding.previewSurface.setZOrderMediaOverlay(true);
+            //binding.previewSurface.setZOrderMediaOverlay(true);
             binding.videoSurface.setZOrderMediaOverlay(false);
         } else {
-            binding.previewSurface.setZOrderMediaOverlay(false);
+            //binding.previewSurface.setZOrderMediaOverlay(false);
             binding.videoSurface.setZOrderMediaOverlay(true);
             binding.videoSurface.setZOrderOnTop(true);
         }
@@ -570,45 +585,36 @@
 
         if (previewWidth == -1 && previewHeight == -1)
             return;
+        mPreviewWidth = previewWidth;
+        mPreviewHeight = previewHeight;
         Log.w(TAG, "resetVideoSize preview: " + previewWidth + "x" + previewHeight);
-        ViewGroup.LayoutParams paramsPreview = binding.previewSurface.getLayoutParams();
-        DisplayMetrics metrics = getResources().getDisplayMetrics();
+    }
 
-        oldW = paramsPreview.width;
-        oldH = paramsPreview.height;
-        double previewMaxDim = Math.max(previewWidth, previewHeight);
-        double previewRatio = metrics.density * 160. / previewMaxDim;
-        paramsPreview.width = (int) (previewWidth * previewRatio);
-        paramsPreview.height = (int) (previewHeight * previewRatio);
-
-        if (oldW != paramsPreview.width || oldH != paramsPreview.height) {
-            Log.w(TAG, "mVideoPreview.setLayoutParams: " + paramsPreview.width + "x" + paramsPreview.height + " was: " + oldW + "x"+oldH);
-            binding.previewSurface.setLayoutParams(paramsPreview);
+    private void configureTransform(int viewWidth, int viewHeight) {
+        Activity activity = getActivity();
+        if (null == binding.previewSurface|| null == activity) {
+            return;
         }
-
-        /*final int mPreviewWidth;
-        final int mPreviewHeight;
-
-        if (mCurrentOrientation == Configuration.ORIENTATION_PORTRAIT) {
-            mPreviewWidth = HardwareServiceImpl.VIDEO_HEIGHT;
-            mPreviewHeight = HardwareServiceImpl.VIDEO_WIDTH;
-        } else {
-            mPreviewWidth = HardwareServiceImpl.VIDEO_WIDTH;
-            mPreviewHeight = HardwareServiceImpl.VIDEO_HEIGHT;
+        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+        boolean rot = Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation;
+        Log.w(TAG, "configureTransform " + viewWidth + "x" + viewHeight + " rot="+rot + " mPreviewWidth="+mPreviewWidth + " mPreviewHeight="+mPreviewHeight);
+        Matrix matrix = new Matrix();
+        RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
+        float centerX = viewRect.centerX();
+        float centerY = viewRect.centerY();
+        if (rot) {
+            RectF bufferRect = new RectF(0, 0, mPreviewHeight, mPreviewWidth);
+            bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
+            matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
+            float scale = Math.max(
+                    (float) viewHeight / mPreviewHeight,
+                    (float) viewWidth / mPreviewWidth);
+            matrix.postScale(scale, scale, centerX, centerY);
+            matrix.postRotate(90 * (rotation - 2), centerX, centerY);
+        } else if (Surface.ROTATION_180 == rotation) {
+            matrix.postRotate(180, centerX, centerY);
         }
-
-        DisplayMetrics metrics = getResources().getDisplayMetrics();
-        RelativeLayout.LayoutParams paramsPreview = (RelativeLayout.LayoutParams) mVideoPreview.getLayoutParams();
-        oldW = paramsPreview.width;
-        oldH = paramsPreview.height;
-        double previewMaxDim = Math.max(mPreviewWidth, mPreviewHeight);
-        double previewRatio = metrics.density * 160. / previewMaxDim;
-        paramsPreview.width = (int) (mPreviewWidth * previewRatio);
-        paramsPreview.height = (int) (mPreviewHeight * previewRatio);
-
-        if (oldW != paramsPreview.width || oldH != paramsPreview.height) {
-            mVideoPreview.setLayoutParams(paramsPreview);
-        }*/
+        binding.previewSurface.setTransform(matrix);
     }
 
     @Override
diff --git a/ring-android/app/src/main/java/cx/ring/services/CameraService.java b/ring-android/app/src/main/java/cx/ring/services/CameraService.java
index 6b8c672..4e0a63b 100644
--- a/ring-android/app/src/main/java/cx/ring/services/CameraService.java
+++ b/ring-android/app/src/main/java/cx/ring/services/CameraService.java
@@ -183,7 +183,7 @@
         void onError();
     }
 
-    abstract void openCamera(Context c, VideoParams videoParams, SurfaceHolder surface, CameraListener listener);
+    abstract void openCamera(Context c, VideoParams videoParams, Object surface, CameraListener listener);
 
     abstract void closeCamera();
 
diff --git a/ring-android/app/src/main/java/cx/ring/services/CameraServiceCamera2.java b/ring-android/app/src/main/java/cx/ring/services/CameraServiceCamera2.java
index fa56b23..ceccee0 100644
--- a/ring-android/app/src/main/java/cx/ring/services/CameraServiceCamera2.java
+++ b/ring-android/app/src/main/java/cx/ring/services/CameraServiceCamera2.java
@@ -20,8 +20,10 @@
 package cx.ring.services;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.ImageFormat;
 import android.graphics.Point;
+import android.graphics.SurfaceTexture;
 import android.hardware.Camera;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
@@ -48,6 +50,8 @@
 import android.view.SurfaceHolder;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import androidx.annotation.NonNull;
@@ -56,6 +60,7 @@
 import cx.ring.daemon.RingserviceJNI;
 import cx.ring.daemon.UintVect;
 import cx.ring.utils.Log;
+import cx.ring.views.AutoFitTextureView;
 
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 class CameraServiceCamera2 extends CameraService {
@@ -200,10 +205,55 @@
         return new Pair<>(codec, encoderInput);
     }
 
-    @Override
-    public void openCamera(Context context, VideoParams videoParams, SurfaceHolder surface, CameraListener listener) {
-        Log.e(TAG, "openCamera " + videoParams.width + "x" + videoParams.height);
+    /**
+     * Compares two {@code Size}s based on their areas.
+     */
+    static 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());
+        }
+
+    }
+
+    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.size() > 0) {
+            return Collections.min(bigEnough, new CompareSizesByArea());
+        } else if (notBigEnough.size() > 0) {
+            return Collections.max(notBigEnough, new CompareSizesByArea());
+        } else {
+            android.util.Log.e(TAG, "Couldn't find any suitable preview size");
+            return choices[0];
+        }
+    }
+
+    @Override
+    public void openCamera(Context context, VideoParams videoParams, Object surface, CameraListener listener) {
         CameraDevice camera = previewCamera;
         if (camera != null) {
             camera.close();
@@ -220,14 +270,41 @@
             Size[] sizes = streamConfigs.getOutputSizes(SurfaceHolder.class);
             for (Size s : sizes)
                 Log.w(TAG, "supportedSize: " + s);
-
+            AutoFitTextureView view = (AutoFitTextureView) surface;
+            //view.getSurfaceTexture().
             boolean flip = videoParams.rotation % 180 != 0;
-            surface.setFixedSize(flip ? videoParams.height : videoParams.width, flip ? videoParams.width : videoParams.height);
-            Surface s = surface.getSurface();
+
+            Size previewSize = chooseOptimalSize(sizes,
+                    flip ? view.getHeight() : view.getWidth(), flip ? view.getWidth() : view.getHeight(),
+                    videoParams.width, videoParams.height,
+                    new Size(videoParams.width, videoParams.height));
+
+            //videoParams.
+            Log.e(TAG, "openCamera " + videoParams.width + "x" + videoParams.height + " flip:" + flip);
+            Log.e(TAG, "openCamera " + videoParams.rotWidth + "x" + videoParams.rotHeight);
+            Log.e(TAG, "openCamera view " + view.getWidth() + "x" + view.getHeight());
+            Log.e(TAG, "openCamera previewSize " + previewSize.getWidth() + "x" + previewSize.getHeight());
+
+            //view.setAspectRatio(flip ? videoParams.height : videoParams.width, flip ? videoParams.width : videoParams.height);
+            //view.setAspectRatio(videoParams.height, videoParams.width);
+            int orientation = context.getResources().getConfiguration().orientation;
+            if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                view.setAspectRatio(
+                        previewSize.getWidth(), previewSize.getHeight());
+            } else {
+                view.setAspectRatio(
+                        previewSize.getHeight(), previewSize.getWidth());
+            }
+
+            SurfaceTexture texture = view.getSurfaceTexture();
+            //texture.setDefaultBufferSize(flip ? previewSize.getHeight() : previewSize.getWidth(), flip ? previewSize.getWidth() : previewSize.getHeight());
+
+            Surface s = new Surface(texture);
+            //s.setFixedSize(flip ? videoParams.height : videoParams.width, flip ? videoParams.width : videoParams.height);
 
             Pair<MediaCodec, Surface> codec = USE_HARDWARE_ENCODER ? openCameraWithEncoder(videoParams, MediaFormat.MIMETYPE_VIDEO_VP8) : null;
 
-            List<Surface> targets = new ArrayList<>(2);
+            final List<Surface> targets = new ArrayList<>(2);
             targets.add(s);
             ImageReader tmpReader = null;
             if (codec != null && codec.second != null) {
@@ -249,6 +326,7 @@
                     try {
                         Log.w(TAG, "onOpened");
                         previewCamera = camera;
+                        texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
                         CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                         builder.addTarget(s);
                         if (codec != null && codec.second != null) {
@@ -259,6 +337,7 @@
                         builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
                         builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
                         builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
+                        //builder.set(CaptureRequest.);
                         final CaptureRequest request = builder.build();
 
                         camera.createCaptureSession(targets, new CameraCaptureSession.StateCallback() {
diff --git a/ring-android/app/src/main/java/cx/ring/services/CameraServiceKitKat.java b/ring-android/app/src/main/java/cx/ring/services/CameraServiceKitKat.java
index 48b8ffa..522324e 100644
--- a/ring-android/app/src/main/java/cx/ring/services/CameraServiceKitKat.java
+++ b/ring-android/app/src/main/java/cx/ring/services/CameraServiceKitKat.java
@@ -66,7 +66,7 @@
     }
 
     @Override
-    public void openCamera(Context c, VideoParams videoParams, SurfaceHolder surface, CameraListener listener) {
+    public void openCamera(Context c, VideoParams videoParams, Object surface, CameraListener listener) {
         final Camera preview;
         try {
             if (previewCamera != null) {
@@ -82,7 +82,7 @@
         }
 
         try {
-            preview.setPreviewDisplay(surface);
+            preview.setPreviewDisplay((SurfaceHolder) surface);
         } catch (IOException e) {
             Log.e(TAG, "setPreviewDisplay: " + e.getMessage());
             return;
diff --git a/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.java
index ffe125d..61adc3a 100644
--- a/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/HardwareServiceImpl.java
@@ -33,6 +33,7 @@
 
 import androidx.annotation.Nullable;
 
+import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.view.WindowManager;
 
@@ -64,7 +65,7 @@
     public static final int VIDEO_HEIGHT = 480;
 
     private static final String TAG = HardwareServiceImpl.class.getName();
-    private static WeakReference<SurfaceHolder> mCameraPreviewSurface = new WeakReference<>(null);
+    private static WeakReference<Object> mCameraPreviewSurface = new WeakReference<>(null);
     private static final Map<String, WeakReference<SurfaceHolder>> videoSurfaces = Collections.synchronizedMap(new HashMap<String, WeakReference<SurfaceHolder>>());
     private final Map<String, Shm> videoInputs = new HashMap<>();
     private final Context mContext;
@@ -380,7 +381,7 @@
             Log.w(TAG, "startCapture: no video parameters ");
             return;
         }
-        final SurfaceHolder surface = mCameraPreviewSurface.get();
+        final Object surface = mCameraPreviewSurface.get();
         if (surface == null) {
             Log.w(TAG, "Can't start capture: no surface registered.");
             cameraService.setPreviewParams(videoParams);
@@ -413,7 +414,6 @@
         event.w = s ? videoParams.height : videoParams.width;
         event.h = s ? videoParams.width : videoParams.height;
         videoEvents.onNext(event);
-
     }
 
     @Override
@@ -471,14 +471,15 @@
 
     @Override
     public void addPreviewVideoSurface(Object oholder) {
-        if (!(oholder instanceof SurfaceHolder)) {
+        /*if (!(oholder instanceof SurfaceHolder)) {
             return;
         }
-        SurfaceHolder holder = (SurfaceHolder)oholder;
-        Log.w(TAG, "addPreviewVideoSurface " + holder.hashCode() + " mCapturingId " + mCapturingId);
-        if (mCameraPreviewSurface.get() == holder)
+        SurfaceHolder holder = (SurfaceHolder)oholder;*/
+        //Surface holder = (Surface) oholder;
+        Log.w(TAG, "addPreviewVideoSurface " + oholder.hashCode() + " mCapturingId " + mCapturingId);
+        if (mCameraPreviewSurface.get() == oholder)
             return;
-        mCameraPreviewSurface = new WeakReference<>(holder);
+        mCameraPreviewSurface = new WeakReference<>(oholder);
         if (mShouldCapture && !mIsCapturing) {
             startCapture(mCapturingId);
         }
diff --git a/ring-android/app/src/main/java/cx/ring/views/AutoFitTextureView.java b/ring-android/app/src/main/java/cx/ring/views/AutoFitTextureView.java
new file mode 100644
index 0000000..cc17996
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/views/AutoFitTextureView.java
@@ -0,0 +1,105 @@
+package cx.ring.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.TextureView;
+
+/**
+ * A {@link TextureView} that can be adjusted to a specified aspect ratio.
+ */
+public class AutoFitTextureView extends TextureView {
+
+    private int mRatioWidth = 720;
+    private int mRatioHeight = 1280;
+    private final int mSize;
+
+    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);
+        mSize = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 192, context.getResources().getDisplayMetrics()));
+    }
+
+    /**
+     * 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.");
+        }
+        Log.w("AutoFitTextureView", "setAspectRatio " + width + "x" + height);
+        mRatioWidth = width;
+        mRatioHeight = height;
+        requestLayout();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        Log.w("AutoFitTextureView", "onMeasure " + widthMeasureSpec + " " + heightMeasureSpec);
+        //int width = MeasureSpec.getSize(widthMeasureSpec);
+        //int height = MeasureSpec.getSize(heightMeasureSpec);
+        int width = Math.min(MeasureSpec.getSize(widthMeasureSpec), mSize);
+        int height = Math.min(MeasureSpec.getSize(heightMeasureSpec), mSize);
+        //MeasureSpec.getMode(widthMeasureSpec);
+        if (0 == mRatioWidth || 0 == mRatioHeight) {
+            setMeasuredDimension(width, height);
+        } else {
+            /*int maxDim = Math.min(Math.max(width, height), mSize);
+            if (width > height) {
+                setMeasuredDimension(maxDim, maxDim * mRatioHeight / mRatioWidth);
+            } else {
+                setMeasuredDimension(maxDim * mRatioWidth / mRatioHeight, maxDim);
+            }*/
+            if (width < height * mRatioWidth / mRatioHeight) {
+                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
+            } else {
+                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
+            }
+        }
+    }
+    /*@Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        Log.w("AutoFitTextureView", "onMeasure " + width + "x" + height);
+        //width = Math.min(width, mSize);
+        //height = Math.min(height, mSize);
+        if (0 != mRatioWidth && 0 != mRatioHeight) {
+            int wrh = width * mRatioHeight;
+            int hrw = height * mRatioWidth;
+            //if (wrh == hrw) {
+            //    setMeasuredDimension(width, height);
+            //    return;
+            //}
+            float w, h;
+            if (wrh < hrw) {
+                w = width;
+                h = wrh / (float)mRatioWidth;
+            } else {
+                w = hrw / (float)mRatioHeight;
+                h = height;
+            }
+            //width = Math.round(w);
+            //height = Math.round(h);
+            Log.w("AutoFitTextureView", "setMeasuredDimension " + width + "x" + height + " ratio: " + (width > height ? height/(float)width : width/(float)height));
+            setMeasuredDimension((int)w, (int)h);
+        } else {
+            setMeasuredDimension(0, 0);
+        }
+    }*/
+}
diff --git a/ring-android/app/src/main/res/layout-land/frag_call.xml b/ring-android/app/src/main/res/layout-land/frag_call.xml
index bda5f2c..701e04f 100644
--- a/ring-android/app/src/main/res/layout-land/frag_call.xml
+++ b/ring-android/app/src/main/res/layout-land/frag_call.xml
@@ -46,15 +46,23 @@
                 android:visibility="gone"
                 tools:visibility="visible" />
 
-            <SurfaceView
-                android:id="@+id/preview_surface"
-                android:layout_width="160dp"
-                android:layout_height="120dp"
+            <androidx.cardview.widget.CardView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:layout_alignParentEnd="true"
                 android:layout_alignParentBottom="true"
                 android:layout_margin="12dp"
-                android:visibility="gone"
-                tools:visibility="visible" />
+                app:cardCornerRadius="16dp"
+                app:cardPreventCornerOverlap="false">
+
+                <cx.ring.views.AutoFitTextureView
+                    android:id="@+id/preview_surface"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="visible"
+                    tools:visibility="visible" />
+            </androidx.cardview.widget.CardView>
+
         </RelativeLayout>
 
         <RelativeLayout
@@ -216,7 +224,7 @@
                     android:layout_height="wrap_content"
                     android:layout_margin="12dp"
                     android:background="@drawable/call_button_background"
-                    android:contentDescription="Mute microphone"
+                    android:contentDescription="@string/action_call_mic_mute"
                     android:onClick="@{() -> presenter.micClicked()}"
                     android:padding="16dp"
                     android:tint="@color/white"
diff --git a/ring-android/app/src/main/res/layout-w720dp-land/tv_frag_call.xml b/ring-android/app/src/main/res/layout-w720dp-land/tv_frag_call.xml
index 1f5ba41..db2cec6 100644
--- a/ring-android/app/src/main/res/layout-w720dp-land/tv_frag_call.xml
+++ b/ring-android/app/src/main/res/layout-w720dp-land/tv_frag_call.xml
@@ -35,15 +35,22 @@
         android:visibility="gone"
         tools:visibility="visible" />
 
-    <SurfaceView
-        android:id="@+id/preview_surface"
-        android:layout_width="160dp"
-        android:layout_height="120dp"
-        android:layout_alignParentBottom="true"
+    <androidx.cardview.widget.CardView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:layout_alignParentEnd="true"
-        android:layout_margin="8dp"
-        android:visibility="gone"
-        tools:visibility="visible" />
+        android:layout_alignParentBottom="true"
+        android:layout_margin="12dp"
+        app:cardCornerRadius="16dp"
+        app:cardPreventCornerOverlap="false">
+
+        <cx.ring.views.AutoFitTextureView
+            android:id="@+id/preview_surface"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="visible"
+            tools:visibility="visible" />
+    </androidx.cardview.widget.CardView>
 
     <LinearLayout
         android:id="@+id/contact_bubble_layout"
diff --git a/ring-android/app/src/main/res/layout/frag_call.xml b/ring-android/app/src/main/res/layout/frag_call.xml
index fc7ea624..beebdd0 100644
--- a/ring-android/app/src/main/res/layout/frag_call.xml
+++ b/ring-android/app/src/main/res/layout/frag_call.xml
@@ -46,15 +46,23 @@
                 android:visibility="gone"
                 tools:visibility="visible" />
 
-            <SurfaceView
-                android:id="@+id/preview_surface"
-                android:layout_width="160dp"
-                android:layout_height="120dp"
+            <androidx.cardview.widget.CardView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:layout_alignParentEnd="true"
                 android:layout_alignParentBottom="true"
                 android:layout_margin="12dp"
-                android:visibility="gone"
-                tools:visibility="visible" />
+                app:cardCornerRadius="16dp"
+                app:cardPreventCornerOverlap="false">
+
+                <cx.ring.views.AutoFitTextureView
+                    android:id="@+id/preview_surface"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="visible"
+                    tools:visibility="visible" />
+            </androidx.cardview.widget.CardView>
+
         </RelativeLayout>
 
         <RelativeLayout
@@ -206,7 +214,7 @@
                     android:layout_height="wrap_content"
                     android:layout_margin="12dp"
                     android:background="@drawable/call_button_background"
-                    android:contentDescription="Mute microphone"
+                    android:contentDescription="@string/action_call_mic_mute"
                     android:onClick="@{() -> presenter.micClicked()}"
                     android:padding="16dp"
                     android:tint="@color/white"
@@ -243,13 +251,13 @@
                 android:layout_centerHorizontal="true"
                 android:layout_marginBottom="4dp"
                 android:contentDescription="@string/action_call_hangup"
+                android:onClick="@{() -> presenter.hangUpClicked()}"
                 app:backgroundTint="@color/error_red"
                 app:elevation="6dp"
                 app:fabSize="normal"
                 app:pressedTranslationZ="12dp"
                 app:rippleColor="@android:color/white"
                 app:srcCompat="@drawable/ic_call_end_white"
-                android:onClick="@{() -> presenter.hangUpClicked()}"
                 app:useCompatPadding="true" />
 
         </RelativeLayout>
diff --git a/ring-android/app/src/main/res/layout/tv_frag_call.xml b/ring-android/app/src/main/res/layout/tv_frag_call.xml
index 1f5ba41..db2cec6 100644
--- a/ring-android/app/src/main/res/layout/tv_frag_call.xml
+++ b/ring-android/app/src/main/res/layout/tv_frag_call.xml
@@ -35,15 +35,22 @@
         android:visibility="gone"
         tools:visibility="visible" />
 
-    <SurfaceView
-        android:id="@+id/preview_surface"
-        android:layout_width="160dp"
-        android:layout_height="120dp"
-        android:layout_alignParentBottom="true"
+    <androidx.cardview.widget.CardView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:layout_alignParentEnd="true"
-        android:layout_margin="8dp"
-        android:visibility="gone"
-        tools:visibility="visible" />
+        android:layout_alignParentBottom="true"
+        android:layout_margin="12dp"
+        app:cardCornerRadius="16dp"
+        app:cardPreventCornerOverlap="false">
+
+        <cx.ring.views.AutoFitTextureView
+            android:id="@+id/preview_surface"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="visible"
+            tools:visibility="visible" />
+    </androidx.cardview.widget.CardView>
 
     <LinearLayout
         android:id="@+id/contact_bubble_layout"
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index e2dcf24..89e6177 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -130,6 +130,7 @@
     <string name="start_error_mic_required">Ring requires the microphone permission to work.</string>
     <string name="action_call_accept">Take call</string>
     <string name="action_call_decline">Decline</string>
+    <string name="action_call_mic_mute">Mute microphone</string>
     <string name="ab_action_speakerphone">Enable speaker</string>
     <string name="ab_action_contact_add">Add to contacts</string>
     <string name="ab_action_contact_add_question">Add to contacts?</string>
diff --git a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
index c68aa4a..7dab460 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/call/CallPresenter.java
@@ -163,6 +163,9 @@
     }
 
     public void screenRotationClick() {
+        //getView().resetVideoSize(videoWidth, videoHeight, previewHeight, previewWidth);
+        mHardwareService.stopCapture();
+
         getView().changeScreenRotation();
     }
 
@@ -229,7 +232,7 @@
     }
 
     public void layoutChanged() {
-        getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
+        //getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
     }
 
     public void uiVisibilityChanged(boolean displayed) {
@@ -321,7 +324,9 @@
                 previewHeight = event.h;
             }
         }
-        getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
+        if (event.started || event.start) {
+            getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
+        }
     }
 
     public void positiveButtonClicked() {