1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import android.graphics.Bitmap;
20import android.graphics.Matrix;
21import android.graphics.RectF;
22import android.graphics.SurfaceTexture;
23import android.view.TextureView;
24import android.view.View;
25import android.view.View.OnLayoutChangeListener;
26
27import com.android.camera.app.AppController;
28import com.android.camera.app.CameraProvider;
29import com.android.camera.app.OrientationManager;
30import com.android.camera.debug.Log;
31import com.android.camera.device.CameraId;
32import com.android.camera.ui.PreviewStatusListener;
33import com.android.camera.util.ApiHelper;
34import com.android.camera.util.CameraUtil;
35import com.android.camera2.R;
36import com.android.ex.camera2.portability.CameraDeviceInfo;
37
38import java.util.ArrayList;
39import java.util.List;
40
41/**
42 * This class aims to automate TextureView transform change and notify listeners
43 * (e.g. bottom bar) of the preview size change.
44 */
45public class TextureViewHelper implements TextureView.SurfaceTextureListener,
46        OnLayoutChangeListener {
47
48    private static final Log.Tag TAG = new Log.Tag("TexViewHelper");
49    public static final float MATCH_SCREEN = 0f;
50    private static final int UNSET = -1;
51    private final TextureView mPreview;
52    private final CameraProvider mCameraProvider;
53    private int mWidth = 0;
54    private int mHeight = 0;
55    private RectF mPreviewArea = new RectF();
56    private float mAspectRatio = MATCH_SCREEN;
57    private boolean mAutoAdjustTransform = true;
58    private TextureView.SurfaceTextureListener mSurfaceTextureListener;
59
60    private final ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>
61            mAspectRatioChangedListeners =
62            new ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>();
63
64    private final ArrayList<PreviewStatusListener.PreviewAreaChangedListener>
65            mPreviewSizeChangedListeners =
66            new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>();
67    private OnLayoutChangeListener mOnLayoutChangeListener = null;
68    private CaptureLayoutHelper mCaptureLayoutHelper = null;
69    private int mOrientation = UNSET;
70
71    // Hack to allow to know which module is running for b/20694189
72    private final AppController mAppController;
73    private final int mCameraModeId;
74    private final int mCaptureIntentModeId;
75
76    public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper,
77            CameraProvider cameraProvider, AppController appController) {
78        mPreview = preview;
79        mCameraProvider = cameraProvider;
80        mPreview.addOnLayoutChangeListener(this);
81        mPreview.setSurfaceTextureListener(this);
82        mCaptureLayoutHelper = helper;
83        mAppController = appController;
84        mCameraModeId = appController.getAndroidContext().getResources()
85                .getInteger(R.integer.camera_mode_photo);
86        mCaptureIntentModeId = appController.getAndroidContext().getResources()
87                .getInteger(R.integer.camera_mode_capture_intent);
88    }
89
90    /**
91     * If auto adjust transform is enabled, when there is a layout change, the
92     * transform matrix will be automatically adjusted based on the preview
93     * stream aspect ratio in the new layout.
94     *
95     * @param enable whether or not auto adjustment should be enabled
96     */
97    public void setAutoAdjustTransform(boolean enable) {
98        mAutoAdjustTransform = enable;
99    }
100
101    @Override
102    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
103            int oldTop, int oldRight, int oldBottom) {
104        Log.v(TAG, "onLayoutChange");
105        int width = right - left;
106        int height = bottom - top;
107        int rotation = CameraUtil.getDisplayRotation();
108        if (mWidth != width || mHeight != height || mOrientation != rotation) {
109            mWidth = width;
110            mHeight = height;
111            mOrientation = rotation;
112            if (!updateTransform()) {
113                clearTransform();
114            }
115        }
116        if (mOnLayoutChangeListener != null) {
117            mOnLayoutChangeListener.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop,
118                    oldRight, oldBottom);
119        }
120    }
121
122    /**
123     * Transforms the preview with the identity matrix, ensuring there is no
124     * scaling on the preview. It also calls onPreviewSizeChanged, to trigger
125     * any necessary preview size changing callbacks.
126     */
127    public void clearTransform() {
128        mPreview.setTransform(new Matrix());
129        mPreviewArea.set(0, 0, mWidth, mHeight);
130        onPreviewAreaChanged(mPreviewArea);
131        setAspectRatio(MATCH_SCREEN);
132    }
133
134    public void updateAspectRatio(float aspectRatio) {
135        Log.v(TAG, "updateAspectRatio " + aspectRatio);
136        if (aspectRatio <= 0) {
137            Log.e(TAG, "Invalid aspect ratio: " + aspectRatio);
138            return;
139        }
140        if (aspectRatio < 1f) {
141            aspectRatio = 1f / aspectRatio;
142        }
143        setAspectRatio(aspectRatio);
144        updateTransform();
145    }
146
147    private void setAspectRatio(float aspectRatio) {
148        Log.v(TAG, "setAspectRatio: " + aspectRatio);
149        if (mAspectRatio != aspectRatio) {
150            Log.v(TAG, "aspect ratio changed from: " + mAspectRatio);
151            mAspectRatio = aspectRatio;
152            onAspectRatioChanged();
153        }
154    }
155
156    private void onAspectRatioChanged() {
157        mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio);
158        for (PreviewStatusListener.PreviewAspectRatioChangedListener listener
159                : mAspectRatioChangedListeners) {
160            listener.onPreviewAspectRatioChanged(mAspectRatio);
161        }
162    }
163
164    public void addAspectRatioChangedListener(
165            PreviewStatusListener.PreviewAspectRatioChangedListener listener) {
166        if (listener != null && !mAspectRatioChangedListeners.contains(listener)) {
167            mAspectRatioChangedListeners.add(listener);
168        }
169    }
170
171    /**
172     * This returns the rect that is available to display the preview, and
173     * capture buttons
174     *
175     * @return the rect.
176     */
177    public RectF getFullscreenRect() {
178        return mCaptureLayoutHelper.getFullscreenRect();
179    }
180
181    /**
182     * This takes a matrix to apply to the texture view and uses the screen
183     * aspect ratio as the target aspect ratio
184     *
185     * @param matrix the matrix to apply
186     * @param aspectRatio the aspectRatio that the preview should be
187     */
188    public void updateTransformFullScreen(Matrix matrix, float aspectRatio) {
189        aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
190        if (aspectRatio != mAspectRatio) {
191            setAspectRatio(aspectRatio);
192        }
193
194        mPreview.setTransform(matrix);
195        mPreviewArea = mCaptureLayoutHelper.getPreviewRect();
196        onPreviewAreaChanged(mPreviewArea);
197
198    }
199
200    public void updateTransform(Matrix matrix) {
201        RectF previewRect = new RectF(0, 0, mWidth, mHeight);
202        matrix.mapRect(previewRect);
203
204        float previewWidth = previewRect.width();
205        float previewHeight = previewRect.height();
206        if (previewHeight == 0 || previewWidth == 0) {
207            Log.e(TAG, "Invalid preview size: " + previewWidth + " x " + previewHeight);
208            return;
209        }
210        float aspectRatio = previewWidth / previewHeight;
211        aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
212        if (aspectRatio != mAspectRatio) {
213            setAspectRatio(aspectRatio);
214        }
215
216        RectF previewAreaBasedOnAspectRatio = mCaptureLayoutHelper.getPreviewRect();
217        Matrix addtionalTransform = new Matrix();
218        addtionalTransform.setRectToRect(previewRect, previewAreaBasedOnAspectRatio,
219                Matrix.ScaleToFit.CENTER);
220        matrix.postConcat(addtionalTransform);
221        mPreview.setTransform(matrix);
222        updatePreviewArea(matrix);
223    }
224
225    /**
226     * Calculates and updates the preview area rect using the latest transform
227     * matrix.
228     */
229    private void updatePreviewArea(Matrix matrix) {
230        mPreviewArea.set(0, 0, mWidth, mHeight);
231        matrix.mapRect(mPreviewArea);
232        onPreviewAreaChanged(mPreviewArea);
233    }
234
235    public void setOnLayoutChangeListener(OnLayoutChangeListener listener) {
236        mOnLayoutChangeListener = listener;
237    }
238
239    public void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener) {
240        mSurfaceTextureListener = listener;
241    }
242
243    /**
244     * Returns a transformation matrix that implements rotation that is
245     * consistent with CaptureLayoutHelper and TextureViewHelper. The magical
246     * invariant for CaptureLayoutHelper and TextureViewHelper that must be
247     * obeyed is that the bounding box of the view must be EXACTLY the bounding
248     * box of the surfaceDimensions AFTER the transformation has been applied.
249     *
250     * @param currentDisplayOrientation The current display orientation,
251     *            measured counterclockwise from to the device's natural
252     *            orientation (in degrees, always a multiple of 90, and between
253     *            0 and 270, inclusive).
254     * @param surfaceDimensions The dimensions of the
255     *            {@link android.view.Surface} on which the preview image is
256     *            being rendered. It usually only makes sense for the upper-left
257     *            corner to be at the origin.
258     * @param desiredBounds The boundaries within the
259     *            {@link android.view.Surface} where the final image should
260     *            appear. These can be used to translate and scale the output,
261     *            but note that the image will be stretched to fit, possibly
262     *            changing its aspect ratio.
263     * @return The transform matrix that should be applied to the
264     *         {@link android.view.Surface} in order for the image to display
265     *         properly in the device's current orientation.
266     */
267    public Matrix getPreviewRotationalTransform(int currentDisplayOrientation,
268            RectF surfaceDimensions,
269            RectF desiredBounds) {
270        if (surfaceDimensions.equals(desiredBounds)) {
271            return new Matrix();
272        }
273
274        Matrix transform = new Matrix();
275        transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL);
276
277        RectF normalRect = surfaceDimensions;
278        // Bounding box of 90 or 270 degree rotation.
279        RectF rotatedRect = new RectF(normalRect.width() / 2 - normalRect.height() / 2,
280                normalRect.height() / 2 - normalRect.width() / 2,
281                normalRect.width() / 2 + normalRect.height() / 2,
282                normalRect.height() / 2 + normalRect.width() / 2);
283
284        OrientationManager.DeviceOrientation deviceOrientation =
285                OrientationManager.DeviceOrientation.from(currentDisplayOrientation);
286
287        // This rotation code assumes that the aspect ratio of the content
288        // (not of necessarily the surface) equals the aspect ratio of view that is receiving
289        // the preview.  So, a 4:3 surface that contains 16:9 data will look correct as
290        // long as the view is also 16:9.
291        switch (deviceOrientation) {
292            case CLOCKWISE_90:
293                transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
294                transform.preRotate(270, mWidth / 2, mHeight / 2);
295                break;
296            case CLOCKWISE_180:
297                transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
298                transform.preRotate(180, mWidth / 2, mHeight / 2);
299                break;
300            case CLOCKWISE_270:
301                transform.setRectToRect(rotatedRect, desiredBounds, Matrix.ScaleToFit.FILL);
302                transform.preRotate(90, mWidth / 2, mHeight / 2);
303                break;
304            case CLOCKWISE_0:
305            default:
306                transform.setRectToRect(normalRect, desiredBounds, Matrix.ScaleToFit.FILL);
307                break;
308        }
309
310        return transform;
311    }
312
313    /**
314     * Updates the transform matrix based current width and height of
315     * TextureView and preview stream aspect ratio.
316     * <p>
317     * If not {@code mAutoAdjustTransform}, this does nothing except return
318     * {@code false}. In all other cases, it returns {@code true}, regardless of
319     * whether the transform was changed.
320     * </p>
321     * In {@code mAutoAdjustTransform} and the CameraProvder is invalid, it is assumed
322     * that the CaptureModule/PhotoModule is Camera2 API-based and must implements its
323     * rotation via matrix transformation implemented in getPreviewRotationalTransform.
324     *
325     * @return Whether {@code mAutoAdjustTransform}.
326     */
327    private boolean updateTransform() {
328        Log.v(TAG, "updateTransform");
329        if (!mAutoAdjustTransform) {
330            return false;
331        }
332
333        if (mAspectRatio == MATCH_SCREEN || mAspectRatio < 0 || mWidth == 0 || mHeight == 0) {
334            return true;
335        }
336
337        Matrix matrix = new Matrix();
338        CameraId cameraKey = mCameraProvider.getCurrentCameraId();
339        int cameraId = -1;
340
341        try {
342            cameraId = cameraKey.getLegacyValue();
343        } catch (UnsupportedOperationException ignored) {
344            Log.e(TAG, "TransformViewHelper does not support Camera API2");
345        }
346
347
348        // Only apply this fix when Current Active Module is Photo module AND
349        // Phone is Nexus4 The enhancement fix b/20694189 to original fix to
350        // b/19271661 ensures that the fix should only be applied when:
351        // 1) the phone is a Nexus4 which requires the specific workaround
352        // 2) CaptureModule is enabled.
353        // 3) the Camera Photo Mode Or Capture Intent Photo Mode is active
354        if (ApiHelper.IS_NEXUS_4 && mAppController.getCameraFeatureConfig().isUsingCaptureModule()
355                && (mAppController.getCurrentModuleIndex() == mCameraModeId ||
356                mAppController.getCurrentModuleIndex() == mCaptureIntentModeId)) {
357            Log.v(TAG, "Applying Photo Mode, Capture Module, Nexus-4 specific fix for b/19271661");
358            mOrientation = CameraUtil.getDisplayRotation();
359            matrix = getPreviewRotationalTransform(mOrientation,
360                    new RectF(0, 0, mWidth, mHeight),
361                    mCaptureLayoutHelper.getPreviewRect());
362        } else if (cameraId >= 0) {
363            // Otherwise, do the default, legacy action.
364            CameraDeviceInfo.Characteristics info = mCameraProvider.getCharacteristics(cameraId);
365            matrix = info.getPreviewTransform(mOrientation, new RectF(0, 0, mWidth, mHeight),
366                    mCaptureLayoutHelper.getPreviewRect());
367        } else {
368            // Do Nothing
369        }
370
371        mPreview.setTransform(matrix);
372        updatePreviewArea(matrix);
373        return true;
374    }
375
376    private void onPreviewAreaChanged(final RectF previewArea) {
377        // Notify listeners of preview area change
378        final List<PreviewStatusListener.PreviewAreaChangedListener> listeners =
379                new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>(
380                        mPreviewSizeChangedListeners);
381        // This method can be called during layout pass. We post a Runnable so
382        // that the callbacks won't happen during the layout pass.
383        mPreview.post(new Runnable() {
384            @Override
385            public void run() {
386                for (PreviewStatusListener.PreviewAreaChangedListener listener : listeners) {
387                    listener.onPreviewAreaChanged(previewArea);
388                }
389            }
390        });
391    }
392
393    /**
394     * Returns a new copy of the preview area, to avoid internal data being
395     * modified from outside of the class.
396     */
397    public RectF getPreviewArea() {
398        return new RectF(mPreviewArea);
399    }
400
401    /**
402     * Returns a copy of the area of the whole preview, including bits clipped
403     * by the view
404     */
405    public RectF getTextureArea() {
406
407        if (mPreview == null) {
408            return new RectF();
409        }
410        Matrix matrix = new Matrix();
411        RectF area = new RectF(0, 0, mWidth, mHeight);
412        mPreview.getTransform(matrix).mapRect(area);
413        return area;
414    }
415
416    public Bitmap getPreviewBitmap(int downsample) {
417        RectF textureArea = getTextureArea();
418        int width = (int) textureArea.width() / downsample;
419        int height = (int) textureArea.height() / downsample;
420        Bitmap preview = mPreview.getBitmap(width, height);
421        return Bitmap.createBitmap(preview, 0, 0, width, height, mPreview.getTransform(null), true);
422    }
423
424    /**
425     * Adds a listener that will get notified when the preview area changed.
426     * This can be useful for UI elements or focus overlay to adjust themselves
427     * according to the preview area change.
428     * <p/>
429     * Note that a listener will only be added once. A newly added listener will
430     * receive a notification of current preview area immediately after being
431     * added.
432     * <p/>
433     * This function should be called on the UI thread and listeners will be
434     * notified on the UI thread.
435     *
436     * @param listener the listener that will get notified of preview area
437     *            change
438     */
439    public void addPreviewAreaSizeChangedListener(
440            PreviewStatusListener.PreviewAreaChangedListener listener) {
441        if (listener != null && !mPreviewSizeChangedListeners.contains(listener)) {
442            mPreviewSizeChangedListeners.add(listener);
443            if (mPreviewArea.width() == 0 || mPreviewArea.height() == 0) {
444                listener.onPreviewAreaChanged(new RectF(0, 0, mWidth, mHeight));
445            } else {
446                listener.onPreviewAreaChanged(new RectF(mPreviewArea));
447            }
448        }
449    }
450
451    /**
452     * Removes a listener that gets notified when the preview area changed.
453     *
454     * @param listener the listener that gets notified of preview area change
455     */
456    public void removePreviewAreaSizeChangedListener(
457            PreviewStatusListener.PreviewAreaChangedListener listener) {
458        if (listener != null && mPreviewSizeChangedListeners.contains(listener)) {
459            mPreviewSizeChangedListeners.remove(listener);
460        }
461    }
462
463    @Override
464    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
465        // Workaround for b/11168275, see b/10981460 for more details
466        if (mWidth != 0 && mHeight != 0) {
467            // Re-apply transform matrix for new surface texture
468            updateTransform();
469        }
470        if (mSurfaceTextureListener != null) {
471            mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height);
472        }
473    }
474
475    @Override
476    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
477        if (mSurfaceTextureListener != null) {
478            mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height);
479        }
480    }
481
482    @Override
483    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
484        if (mSurfaceTextureListener != null) {
485            mSurfaceTextureListener.onSurfaceTextureDestroyed(surface);
486        }
487        return false;
488    }
489
490    @Override
491    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
492        if (mSurfaceTextureListener != null) {
493            mSurfaceTextureListener.onSurfaceTextureUpdated(surface);
494        }
495
496    }
497}
498