Merge "Device quirk: preview stretched on Samsung J5 Prime" into androidx-main
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
index ba1c76f..9bc5023 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
@@ -113,7 +113,7 @@
             SURFACE_SIZE,
             BACK_CAMERA
         )
-        return mPreviewTransform.isCropRectAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
+        return mPreviewTransform.isViewportAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
     }
 
     @Test
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index ce02f39..39c7819 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -55,7 +55,7 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.ViewPort;
 import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.view.internal.compat.quirk.PreviewStretchedQuirk;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
 import androidx.core.util.Preconditions;
 
 /**
@@ -104,8 +104,12 @@
 
     // SurfaceRequest.getResolution().
     private Size mResolution;
-    // TransformationInfo.getCropRect().
+    // This represents the area of the Surface that should be visible to end users. The value
+    // is based on TransformationInfo.getCropRect() with possible corrections due to device quirks.
     private Rect mSurfaceCropRect;
+    // This rect represents the size of the viewport in preview. It's always the same as
+    // TransformationInfo.getCropRect().
+    private Rect mViewportRect;
     // TransformationInfo.getRotationDegrees().
     private int mPreviewRotationDegrees;
     // TransformationInfo.getTargetRotation.
@@ -128,7 +132,8 @@
             Size resolution, boolean isFrontCamera) {
         Logger.d(TAG, "Transformation info set: " + transformationInfo + " " + resolution + " "
                 + isFrontCamera);
-        mSurfaceCropRect = transformationInfo.getCropRect();
+        mSurfaceCropRect = getCorrectedCropRect(transformationInfo.getCropRect());
+        mViewportRect = transformationInfo.getCropRect();
         mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
         mTargetRotation = transformationInfo.getTargetRotation();
         mResolution = resolution;
@@ -237,7 +242,7 @@
 
         // Get the target of the mapping, the vertices of the crop rect in PreviewView.
         float[] previewViewCropRectVertices;
-        if (isCropRectAspectRatioMatchPreviewView(previewViewSize)) {
+        if (isViewportAspectRatioMatchPreviewView(previewViewSize)) {
             // If crop rect has the same aspect ratio as PreviewView, scale the crop rect to fill
             // the entire PreviewView. This happens if the scale type is FILL_* AND a
             // PreviewView-based viewport is used.
@@ -245,7 +250,7 @@
         } else {
             // If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the
             // Viewport is not based on the PreviewView or 3) both.
-            RectF previewViewCropRect = getPreviewViewCropRectForMismatchedAspectRatios(
+            RectF previewViewCropRect = getPreviewViewViewportRectForMismatchedAspectRatios(
                     previewViewSize, layoutDirection);
             previewViewCropRectVertices = rectToVertices(previewViewCropRect);
         }
@@ -253,7 +258,7 @@
                 previewViewCropRectVertices, mPreviewRotationDegrees);
 
         // Get the source of the mapping, the vertices of the crop rect in Surface.
-        float[] surfaceCropRectVertices = getSurfaceCropRectVertices();
+        float[] surfaceCropRectVertices = rectToVertices(new RectF(mSurfaceCropRect));
 
         // Map source to target.
         matrix.setPolyToPoly(surfaceCropRectVertices, 0, rotatedPreviewViewCropRectVertices, 0, 4);
@@ -282,43 +287,46 @@
     /**
      * Gets the vertices of the crop rect in Surface.
      */
-    private float[] getSurfaceCropRectVertices() {
-        RectF cropRectF = new RectF(mSurfaceCropRect);
-        PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+    private Rect getCorrectedCropRect(Rect surfaceCropRect) {
+        PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
         if (quirk != null) {
             // Correct crop rect if the device has a quirk.
+            RectF cropRectF = new RectF(surfaceCropRect);
             Matrix correction = new Matrix();
             correction.setScale(
                     quirk.getCropRectScaleX(),
-                    quirk.getCropRectScaleY(),
-                    mSurfaceCropRect.centerX(),
-                    mSurfaceCropRect.centerY());
+                    1f,
+                    surfaceCropRect.centerX(),
+                    surfaceCropRect.centerY());
             correction.mapRect(cropRectF);
+            Rect correctRect = new Rect();
+            cropRectF.round(correctRect);
+            return correctRect;
         }
-        return rectToVertices(cropRectF);
+        return surfaceCropRect;
     }
 
     /**
-     * Gets the crop rect in {@link PreviewView} coordinates for the case where crop rect's aspect
-     * ratio doesn't match {@link PreviewView}'s aspect ratio.
+     * Gets the viewport rect in {@link PreviewView} coordinates for the case where viewport's
+     * aspect ratio doesn't match {@link PreviewView}'s aspect ratio.
      *
      * <p> When aspect ratios don't match, additional calculation is needed to figure out how to
      * fit crop rect into the{@link PreviewView}.
      */
-    RectF getPreviewViewCropRectForMismatchedAspectRatios(Size previewViewSize,
+    RectF getPreviewViewViewportRectForMismatchedAspectRatios(Size previewViewSize,
             int layoutDirection) {
         RectF previewViewRect = new RectF(0, 0, previewViewSize.getWidth(),
                 previewViewSize.getHeight());
-        Size rotatedCropRectSize = getRotatedCropRectSize();
-        RectF rotatedSurfaceCropRect = new RectF(0, 0, rotatedCropRectSize.getWidth(),
-                rotatedCropRectSize.getHeight());
+        Size rotatedViewportSize = getRotatedViewportSize();
+        RectF rotatedViewportRect = new RectF(0, 0, rotatedViewportSize.getWidth(),
+                rotatedViewportSize.getHeight());
         Matrix matrix = new Matrix();
-        setMatrixRectToRect(matrix, rotatedSurfaceCropRect, previewViewRect, mScaleType);
-        matrix.mapRect(rotatedSurfaceCropRect);
+        setMatrixRectToRect(matrix, rotatedViewportRect, previewViewRect, mScaleType);
+        matrix.mapRect(rotatedViewportRect);
         if (layoutDirection == LayoutDirection.RTL) {
-            return flipHorizontally(rotatedSurfaceCropRect, (float) previewViewSize.getWidth() / 2);
+            return flipHorizontally(rotatedViewportRect, (float) previewViewSize.getWidth() / 2);
         }
-        return rotatedSurfaceCropRect;
+        return rotatedViewportRect;
     }
 
     /**
@@ -374,29 +382,29 @@
     }
 
     /**
-     * Returns crop rect size with target rotation applied.
+     * Returns viewport size with target rotation applied.
      */
-    private Size getRotatedCropRectSize() {
-        Preconditions.checkNotNull(mSurfaceCropRect);
+    private Size getRotatedViewportSize() {
         if (is90or270(mPreviewRotationDegrees)) {
-            return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
+            return new Size(mViewportRect.height(), mViewportRect.width());
         }
-        return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
+        return new Size(mViewportRect.width(), mViewportRect.height());
     }
 
     /**
-     * Checks if the crop rect's aspect ratio matches that of the {@link PreviewView}.
+     * Checks if the viewport's aspect ratio matches that of the {@link PreviewView}.
      *
      * <p> The mismatch could happen if the {@link ViewPort} is not based on the
      * {@link PreviewView}, or the {@link PreviewView#getScaleType()} is FIT_*. In this case, we
      * need to calculate how the crop rect should be fitted.
      */
     @VisibleForTesting
-    boolean isCropRectAspectRatioMatchPreviewView(Size previewViewSize) {
-        Size rotatedSize = getRotatedCropRectSize();
+    boolean isViewportAspectRatioMatchPreviewView(Size previewViewSize) {
+        // Using viewport rect to check if the viewport is based on the PreviewView.
+        Size rotatedViewportSize = getRotatedViewportSize();
         return isAspectRatioMatchingWithRoundingError(
                 previewViewSize, /* isAccurate1= */ true,
-                rotatedSize,  /* isAccurate2= */ false);
+                rotatedViewportSize,  /* isAccurate2= */ false);
     }
 
     /**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index 1fca5fe0..d73a851 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -39,8 +39,8 @@
         final List<Quirk> quirks = new ArrayList<>();
 
         // Load all device specific quirks
-        if (PreviewStretchedQuirk.load()) {
-            quirks.add(new PreviewStretchedQuirk());
+        if (PreviewOneThirdWiderQuirk.load()) {
+            quirks.add(new PreviewOneThirdWiderQuirk());
         }
 
         if (SurfaceViewStretchedQuirk.load()) {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
new file mode 100644
index 0000000..316e4db
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where the preview buffer is stretched.
+ *
+ * <p> The symptom is, the preview's FOV is always 1/3 wider than intended. For example, if the
+ * preview Surface is 800x600, it's actually has a FOV of 1066x600 with the same center point,
+ * but squeezed to fit the 800x600 buffer.
+ */
+public class PreviewOneThirdWiderQuirk implements Quirk {
+
+    private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
+    private static final String SAMSUNG_J5_PRIME = "ON5XELTE"; // b/183329599
+
+    static boolean load() {
+        boolean isSamsungJ5PrimeAndApi26 =
+                SAMSUNG_J5_PRIME.equals(Build.DEVICE.toUpperCase()) && Build.VERSION.SDK_INT >= 26;
+        boolean isSamsungA3 = SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase());
+        return isSamsungJ5PrimeAndApi26 || isSamsungA3;
+    }
+
+    /**
+     * The mount that the crop rect needs to be scaled in x.
+     */
+    public float getCropRectScaleX() {
+        return 0.75f;
+    }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
deleted file mode 100644
index fd274fa..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.camera.core.impl.Quirk;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A quirk where the preview buffer is stretched.
- *
- * <p> This is similar to the SamsungPreviewTargetAspectRatioQuirk in camera-camera2 artifact.
- * The difference is, the other quirk can be fixed by choosing a different resolution,
- * while for this one the preview is always stretched no matter what resolution is selected.
- */
-public class PreviewStretchedQuirk implements Quirk {
-
-    private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
-
-    private static final List<String> KNOWN_AFFECTED_DEVICES = Arrays.asList(SAMSUNG_A3_2017);
-
-    static boolean load() {
-        return KNOWN_AFFECTED_DEVICES.contains(Build.DEVICE.toUpperCase());
-    }
-
-    /**
-     * The mount that the crop rect needs to be scaled in x.
-     */
-    public float getCropRectScaleX() {
-        if (SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase())) {
-            // For Samsung A3 2017, the symptom seems to be that the preview's FOV is always 1/3
-            // wider than it's supposed to be. For example, if the preview Surface is 800x600, it's
-            // actually has a FOV of 1066x600, but stretched to fit the 800x600 buffer. To correct
-            // the preview, we need to crop out the extra 25% FOV.
-            return 0.75f;
-        }
-        // No scale.
-        return 1;
-    }
-
-    /**
-     * The mount that the crop rect needs to be scaled in y.
-     */
-    public float getCropRectScaleY() {
-        // No scale.
-        return 1;
-    }
-}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
new file mode 100644
index 0000000..803a3a3
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
+import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/**
+ * Unit tests for {@link PreviewTransformation}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class PreviewTransformationTest {
+
+    private static final Rect CROP_RECT = new Rect(0, 0, 600, 400);
+
+    private final PreviewTransformation mPreviewTransformation = new PreviewTransformation();
+
+    @Test
+    public void withPreviewStretchedQuirk_cropRectIsAdjusted() {
+        // Arrange.
+        QuirkInjector.inject(new PreviewOneThirdWiderQuirk());
+
+        // Act.
+        mPreviewTransformation.setTransformationInfo(
+                SurfaceRequest.TransformationInfo.of(CROP_RECT, 0, 0),
+                new Size(CROP_RECT.width(), CROP_RECT.height()),
+                /*isFrontCamera*/ false);
+
+        // Assert: the crop rect is corrected.
+        assertThat(mPreviewTransformation.getSurfaceCropRect()).isEqualTo(new Rect(75, 0, 525,
+                400));
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
similarity index 61%
rename from camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
rename to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
index 0b42944..17c02f5 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
@@ -28,24 +28,36 @@
 import org.robolectric.util.ReflectionHelpers;
 
 /**
- * Unit tests for {@link PreviewStretchedQuirk}.
+ * Unit tests for {@link PreviewOneThirdWiderQuirk}.
  */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewStretchedQuirkTest {
+public class PreviewOneThirdWiderQuirkTest {
 
     @Test
     public void quirkExistsOnSamsungA3() {
-        // Arrange.
         ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
+        assertPreviewShouldBeCroppedBy25Percent();
+    }
 
-        // Act.
-        final PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.O)
+    public void quirkExistsOnSamsungJ5PrimeApi26AndAbove() {
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+        assertPreviewShouldBeCroppedBy25Percent();
+    }
 
-        // Assert.
+    @Test
+    @Config(maxSdk = Build.VERSION_CODES.N_MR1)
+    public void quirkDoesNotExistOnSamsungJ5PrimeApi25AndBelow() {
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+        assertThat(DeviceQuirks.get(PreviewOneThirdWiderQuirk.class)).isNull();
+    }
+
+    private void assertPreviewShouldBeCroppedBy25Percent() {
+        final PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
         assertThat(quirk).isNotNull();
         assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
-        assertThat(quirk.getCropRectScaleY()).isEqualTo(1F);
     }
 }
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
index d15867e..5b18e9d 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
@@ -161,9 +161,13 @@
         // Loop through the y plane and get the sum of the luminance for each tile.
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
         image.getPlanes()[0].getBuffer().get(bytes);
+        int tileX;
+        int tileY;
         for (int x = 0; x < cropRect.width(); x++) {
             for (int y = 0; y < cropRect.height(); y++) {
-                tiles[x / tileWidth][y / tileHeight] +=
+                tileX = Math.min(x / tileWidth, TILE_COUNT - 1);
+                tileY = Math.min(y / tileHeight, TILE_COUNT - 1);
+                tiles[tileX][tileY] +=
                         bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
             }
         }