6/n: Add camera preview to FaceEnrollEnrolling

Bug: 112005540

Test: manual
Change-Id: Ie4f810dffecdec9731e20d5756854d9c9f420f4b
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0a5f24b..51de802 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -94,6 +94,7 @@
     <uses-permission android:name="android.permission.TEST_BLACKLISTED_PASSWORD" />
     <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
     <uses-permission android:name="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS" />
+    <uses-permission android:name="android.permission.CAMERA" />
 
     <application android:label="@string/settings_label"
             android:icon="@drawable/ic_launcher_settings"
@@ -1576,9 +1577,17 @@
             android:windowSoftInputMode="stateHidden|adjustResize"
             android:theme="@style/GlifTheme.Light"/>
 
-        <activity android:name=".biometrics.face.FaceEnrollIntroduction" android:exported="false" />
-        <activity android:name=".biometrics.face.FaceEnrollEnrolling" android:exported="false" />
-        <activity android:name=".biometrics.face.FaceEnrollFinish" android:exported="false" />
+        <activity android:name=".biometrics.face.FaceEnrollIntroduction"
+            android:exported="false"
+            android:screenOrientation="portrait"/>
+
+        <activity android:name=".biometrics.face.FaceEnrollEnrolling"
+            android:exported="false"
+            android:screenOrientation="portrait"/>
+
+        <activity android:name=".biometrics.face.FaceEnrollFinish"
+            android:exported="false"
+            android:screenOrientation="portrait"/>
 
         <activity android:name=".biometrics.fingerprint.FingerprintSettings" android:exported="false"/>
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollFindSensor" android:exported="false"/>
diff --git a/res/layout/face_enroll_enrolling.xml b/res/layout/face_enroll_enrolling.xml
index 6ced80f..2208cc2 100644
--- a/res/layout/face_enroll_enrolling.xml
+++ b/res/layout/face_enroll_enrolling.xml
@@ -39,20 +39,23 @@
             android:gravity="center"
             android:orientation="vertical">
 
-            <com.android.setupwizardlib.view.FillContentLayout
+            <com.android.settings.biometrics.face.FaceSquareFrameLayout
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
+                android:layout_height="match_parent"
                 android:layout_weight="1">
 
-                <!-- TODO: replace this with actual content-->
-                <ImageView
-                    style="@style/SuwContentIllustration"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:contentDescription="@null"
-                    android:src="@drawable/face_enroll_introduction" />
+                <com.android.settings.biometrics.face.FaceSquareTextureView
+                    android:id="@+id/texture_view"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:contentDescription="@null" />
 
-            </com.android.setupwizardlib.view.FillContentLayout>
+                <ImageView
+                    android:id="@+id/circle_view"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent" />
+
+            </com.android.settings.biometrics.face.FaceSquareFrameLayout>
 
             <TextView
                 style="@style/TextAppearance.FaceErrorText"
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java b/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java
new file mode 100644
index 0000000..0da666c
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 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 com.android.settings.biometrics.face;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable containing the circle cutout.
+ */
+public class FaceEnrollAnimationDrawable extends Drawable {
+
+    private Rect mBounds;
+    private final Paint mSquarePaint;
+    private final Paint mCircleCutoutPaint;
+
+    public FaceEnrollAnimationDrawable() {
+        mSquarePaint = new Paint();
+        mSquarePaint.setColor(Color.WHITE);
+        mSquarePaint.setAntiAlias(true);
+
+        mCircleCutoutPaint = new Paint();
+        mCircleCutoutPaint.setColor(Color.TRANSPARENT);
+        mCircleCutoutPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+        mCircleCutoutPaint.setAntiAlias(true);
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        mBounds = bounds;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mBounds == null) {
+            return;
+        }
+        canvas.save();
+
+        // Draw a rectangle covering the whole view
+        canvas.drawRect(0, 0, mBounds.width(), mBounds.height(), mSquarePaint);
+
+        // Clear a circle in the middle for the camera preview
+        canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(),
+                mBounds.height() / 2, mCircleCutoutPaint);
+
+        canvas.restore();
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
index 9786363..7fac9f6 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
@@ -40,10 +40,12 @@
 
     private static final String TAG = "FaceEnrollEnrolling";
     private static final boolean DEBUG = true;
+    private static final String TAG_FACE_PREVIEW = "tag_preview";
 
     private TextView mErrorText;
     private Interpolator mLinearOutSlowInInterpolator;
     private boolean mShouldFinishOnStop = true;
+    private FaceEnrollPreviewFragment mFaceCameraPreview;
 
     public static class FaceErrorDialog extends BiometricErrorDialog {
         static FaceErrorDialog newInstance(CharSequence msg, int msgId) {
@@ -93,6 +95,18 @@
     }
 
     @Override
+    public void startEnrollment() {
+        super.startEnrollment();
+        mFaceCameraPreview = (FaceEnrollPreviewFragment) getSupportFragmentManager()
+                .findFragmentByTag(TAG_FACE_PREVIEW);
+        if (mFaceCameraPreview == null) {
+            mFaceCameraPreview = new FaceEnrollPreviewFragment();
+            getSupportFragmentManager().beginTransaction().add(mFaceCameraPreview, TAG_FACE_PREVIEW)
+                    .commitAllowingStateLoss();
+        }
+    }
+
+    @Override
     protected Intent getFinishIntent() {
         return new Intent(this, FaceEnrollFinish.class);
     }
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java b/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java
new file mode 100644
index 0000000..2b92ccb
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2018 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 com.android.settings.biometrics.face;
+
+import android.content.Context;
+import android.graphics.Matrix;
+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.CaptureRequest;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.core.InstrumentedPreferenceFragment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Fragment that contains the logic for showing and controlling the camera preview, circular
+ * overlay, as well as the enrollment animations.
+ */
+public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
+
+    private static final String TAG = "FaceEnrollPreviewFragment";
+
+    private static final int MAX_PREVIEW_WIDTH = 1920;
+    private static final int MAX_PREVIEW_HEIGHT = 1080;
+
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private CameraManager mCameraManager;
+    private String mCameraId;
+    private CameraDevice mCameraDevice;
+    private CaptureRequest.Builder mPreviewRequestBuilder;
+    private CameraCaptureSession mCaptureSession;
+    private CaptureRequest mPreviewRequest;
+    private Size mPreviewSize;
+
+    // View used to contain the circular cutout and enrollment animation drawable
+    private ImageView mCircleView;
+
+    // Drawable containing the circular cutout and enrollment animations
+    private FaceEnrollAnimationDrawable mAnimationDrawable;
+
+    // Texture used for showing the camera preview
+    private FaceSquareTextureView mTextureView;
+
+    private final TextureView.SurfaceTextureListener mSurfaceTextureListener =
+            new TextureView.SurfaceTextureListener() {
+
+        @Override
+        public void onSurfaceTextureAvailable(
+                SurfaceTexture surfaceTexture, int width, int height) {
+            openCamera(width, height);
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(
+                SurfaceTexture surfaceTexture, int width, int height) {
+            // Shouldn't be called, but do this for completeness.
+            configureTransform(width, height);
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+            return true;
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+
+        }
+    };
+
+    private final CameraDevice.StateCallback mCameraStateCallback =
+            new CameraDevice.StateCallback() {
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            mCameraDevice = cameraDevice;
+
+            try {
+                // Configure the size of default buffer
+                SurfaceTexture texture = mTextureView.getSurfaceTexture();
+                texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+
+                // This is the output Surface we need to start preview
+                Surface surface = new Surface(texture);
+
+                // Set up a CaptureRequest.Builder with the output Surface
+                mPreviewRequestBuilder =
+                        mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+                mPreviewRequestBuilder.addTarget(surface);
+
+                // Create a CameraCaptureSession for camera preview
+                mCameraDevice.createCaptureSession(Arrays.asList(surface),
+                    new CameraCaptureSession.StateCallback() {
+
+                        @Override
+                        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+                            // The camera is already closed
+                            if (null == mCameraDevice) {
+                                return;
+                            }
+                            // When the session is ready, we start displaying the preview.
+                            mCaptureSession = cameraCaptureSession;
+                            try {
+                                // Auto focus should be continuous for camera preview.
+                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+                                // Finally, we start displaying the camera preview.
+                                mPreviewRequest = mPreviewRequestBuilder.build();
+                                mCaptureSession.setRepeatingRequest(mPreviewRequest,
+                                        null /* listener */, mHandler);
+                            } catch (CameraAccessException e) {
+                                Log.e(TAG, "Unable to access camera", e);
+                            }
+                        }
+
+                        @Override
+                        public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+                            Log.e(TAG, "Unable to configure camera");
+                        }
+                    }, null /* handler */);
+            } catch (CameraAccessException e) {
+                e.printStackTrace();
+            }
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            cameraDevice.close();
+            mCameraDevice = null;
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            cameraDevice.close();
+            mCameraDevice = null;
+        }
+    };
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsProto.MetricsEvent.FACE_ENROLL_PREVIEW;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mTextureView = getActivity().findViewById(R.id.texture_view);
+        mCircleView = getActivity().findViewById(R.id.circle_view);
+
+        // Must disable hardware acceleration for this view, otherwise transparency breaks
+        mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+        mAnimationDrawable = new FaceEnrollAnimationDrawable();
+        mCircleView.setImageDrawable(mAnimationDrawable);
+
+        mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        // 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);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        closeCamera();
+    }
+
+    /**
+     * 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) {
+        try {
+            for (String cameraId : mCameraManager.getCameraIdList()) {
+                CameraCharacteristics characteristics =
+                        mCameraManager.getCameraCharacteristics(cameraId);
+
+                // Find front facing camera
+                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+                if (facing == null || facing != CameraCharacteristics.LENS_FACING_FRONT) {
+                    continue;
+                }
+                mCameraId = cameraId;
+
+                // Get the stream configurations
+                StreamConfigurationMap map = characteristics.get(
+                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+                        width, height, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT);
+            }
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to access camera", e);
+        }
+    }
+
+    /**
+     * Opens the camera specified by mCameraId.
+     * @param width  The width of the texture view
+     * @param height The height of the texture view
+     */
+    private void openCamera(int width, int height) {
+        try {
+            setUpCameraOutputs(width, height);
+            mCameraManager.openCamera(mCameraId, mCameraStateCallback, mHandler);
+            configureTransform(width, height);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to open camera", e);
+        }
+    }
+
+    /**
+     * Chooses the optimal resolution for the camera to open.
+     */
+    private Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight,
+            int maxWidth, int maxHeight) {
+        // 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<>();
+
+        for (Size option : choices) {
+            if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+                    option.getHeight() == option.getWidth()) {
+                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 {
+            Log.e(TAG, "Couldn't find any suitable preview size");
+            return choices[0];
+        }
+    }
+
+    /**
+     * 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 (mTextureView == null) {
+            return;
+        }
+
+        // Fix the aspect ratio
+        Matrix matrix = new Matrix();
+        float scaleX = (float) viewWidth / mPreviewSize.getWidth();
+        float scaleY = (float) viewHeight / mPreviewSize.getHeight();
+
+        // Now divide by smaller one so it fills up the original space
+        float smaller = Math.min(scaleX, scaleY);
+        scaleX = scaleX / smaller;
+        scaleY = scaleY / smaller;
+
+        // Apply the scale
+        matrix.setScale(scaleX, scaleY);
+
+        mTextureView.setTransform(matrix);
+    }
+
+    private void closeCamera() {
+        if (mCaptureSession != null) {
+            mCaptureSession.close();
+            mCaptureSession = null;
+        }
+        if (mCameraDevice != null) {
+            mCameraDevice.close();
+            mCameraDevice = null;
+        }
+    }
+
+    /**
+     * Compares two {@code Size}s based on their areas.
+     */
+    private 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());
+        }
+
+    }
+
+}
diff --git a/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java b/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java
new file mode 100644
index 0000000..3aed524
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 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 com.android.settings.biometrics.face;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Square layout that sets the height to be the same as width.
+ */
+public class FaceSquareFrameLayout extends FrameLayout {
+
+    public FaceSquareFrameLayout(Context context) {
+        super(context);
+    }
+
+    public FaceSquareFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FaceSquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public FaceSquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Don't call super, manually set their size below
+        int size = MeasureSpec.getSize(widthMeasureSpec);
+
+        // Set this frame layout to be a square
+        setMeasuredDimension(size, size);
+
+        // Set the children to be the same size (square) as well
+        final int numChildren = getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+            this.getChildAt(i).measure(spec, spec);
+        }
+    }
+}
diff --git a/src/com/android/settings/biometrics/face/FaceSquareTextureView.java b/src/com/android/settings/biometrics/face/FaceSquareTextureView.java
new file mode 100644
index 0000000..ebbbc27
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceSquareTextureView.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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 com.android.settings.biometrics.face;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.TextureView;
+
+/**
+ * A square {@link TextureView}.
+ */
+public class FaceSquareTextureView extends TextureView {
+
+    public FaceSquareTextureView(Context context) {
+        this(context, null);
+    }
+
+    public FaceSquareTextureView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FaceSquareTextureView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (width < height) {
+            setMeasuredDimension(width, width);
+        } else {
+            setMeasuredDimension(height, height);
+        }
+    }
+}