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 
17 package com.android.camera;
18 
19 import android.graphics.Bitmap;
20 import android.graphics.Matrix;
21 import android.graphics.RectF;
22 import android.graphics.SurfaceTexture;
23 import android.view.TextureView;
24 import android.view.View;
25 import android.view.View.OnLayoutChangeListener;
26 
27 import com.android.camera.app.AppController;
28 import com.android.camera.app.CameraProvider;
29 import com.android.camera.app.OrientationManager;
30 import com.android.camera.debug.Log;
31 import com.android.camera.device.CameraId;
32 import com.android.camera.ui.PreviewStatusListener;
33 import com.android.camera.util.ApiHelper;
34 import com.android.camera.util.CameraUtil;
35 import com.android.camera2.R;
36 import com.android.ex.camera2.portability.CameraDeviceInfo;
37 
38 import java.util.ArrayList;
39 import 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  */
45 public 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 
TextureViewHelper(TextureView preview, CaptureLayoutHelper helper, CameraProvider cameraProvider, AppController appController)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      */
setAutoAdjustTransform(boolean enable)97     public void setAutoAdjustTransform(boolean enable) {
98         mAutoAdjustTransform = enable;
99     }
100 
101     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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      */
clearTransform()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 
updateAspectRatio(float aspectRatio)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 
setAspectRatio(float aspectRatio)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 
onAspectRatioChanged()156     private void onAspectRatioChanged() {
157         mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio);
158         for (PreviewStatusListener.PreviewAspectRatioChangedListener listener
159                 : mAspectRatioChangedListeners) {
160             listener.onPreviewAspectRatioChanged(mAspectRatio);
161         }
162     }
163 
addAspectRatioChangedListener( PreviewStatusListener.PreviewAspectRatioChangedListener listener)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      */
getFullscreenRect()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      */
updateTransformFullScreen(Matrix matrix, float aspectRatio)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