1/*
2 * Copyright (C) 2012 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.annotation.TargetApi;
20import android.graphics.SurfaceTexture;
21import android.opengl.Matrix;
22import android.util.Log;
23
24import com.android.gallery3d.common.ApiHelper;
25import com.android.gallery3d.ui.GLCanvas;
26import com.android.gallery3d.ui.RawTexture;
27import com.android.gallery3d.ui.SurfaceTextureScreenNail;
28
29/*
30 * This is a ScreenNail which can display camera's preview.
31 */
32@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
33public class CameraScreenNail extends SurfaceTextureScreenNail {
34    private static final String TAG = "CAM_ScreenNail";
35    private static final int ANIM_NONE = 0;
36    // Capture animation is about to start.
37    private static final int ANIM_CAPTURE_START = 1;
38    // Capture animation is running.
39    private static final int ANIM_CAPTURE_RUNNING = 2;
40    // Switch camera animation needs to copy texture.
41    private static final int ANIM_SWITCH_COPY_TEXTURE = 3;
42    // Switch camera animation shows the initial feedback by darkening the
43    // preview.
44    private static final int ANIM_SWITCH_DARK_PREVIEW = 4;
45    // Switch camera animation is waiting for the first frame.
46    private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5;
47    // Switch camera animation is about to start.
48    private static final int ANIM_SWITCH_START = 6;
49    // Switch camera animation is running.
50    private static final int ANIM_SWITCH_RUNNING = 7;
51
52    private boolean mVisible;
53    // True if first onFrameAvailable has been called. If screen nail is drawn
54    // too early, it will be all white.
55    private boolean mFirstFrameArrived;
56    private Listener mListener;
57    private final float[] mTextureTransformMatrix = new float[16];
58
59    // Animation.
60    private CaptureAnimManager mCaptureAnimManager = new CaptureAnimManager();
61    private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager();
62    private int mAnimState = ANIM_NONE;
63    private RawTexture mAnimTexture;
64    // Some methods are called by GL thread and some are called by main thread.
65    // This protects mAnimState, mVisible, and surface texture. This also makes
66    // sure some code are atomic. For example, requestRender and setting
67    // mAnimState.
68    private Object mLock = new Object();
69
70    private OnFrameDrawnListener mOneTimeFrameDrawnListener;
71    private int mRenderWidth;
72    private int mRenderHeight;
73    // This represents the scaled, uncropped size of the texture
74    // Needed for FaceView
75    private int mUncroppedRenderWidth;
76    private int mUncroppedRenderHeight;
77    private float mScaleX = 1f, mScaleY = 1f;
78    private boolean mFullScreen;
79    private boolean mEnableAspectRatioClamping = false;
80
81    public interface Listener {
82        void requestRender();
83        // Preview has been copied to a texture.
84        void onPreviewTextureCopied();
85
86        void onCaptureTextureCopied();
87    }
88
89    public interface OnFrameDrawnListener {
90        void onFrameDrawn(CameraScreenNail c);
91    }
92
93    public CameraScreenNail(Listener listener) {
94        mListener = listener;
95    }
96
97    public void setFullScreen(boolean full) {
98        synchronized (mLock) {
99            mFullScreen = full;
100        }
101    }
102
103    /**
104     * returns the uncropped, but scaled, width of the rendered texture
105     */
106    public int getUncroppedRenderWidth() {
107        return mUncroppedRenderWidth;
108    }
109
110    /**
111     * returns the uncropped, but scaled, width of the rendered texture
112     */
113    public int getUncroppedRenderHeight() {
114        return mUncroppedRenderHeight;
115    }
116
117    @Override
118    public int getWidth() {
119        return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
120    }
121
122    @Override
123    public int getHeight() {
124        return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
125    }
126
127    private int getTextureWidth() {
128        return super.getWidth();
129    }
130
131    private int getTextureHeight() {
132        return super.getHeight();
133    }
134
135    @Override
136    public void setSize(int w, int h) {
137        super.setSize(w,  h);
138        mEnableAspectRatioClamping = false;
139        if (mRenderWidth == 0) {
140            mRenderWidth = w;
141            mRenderHeight = h;
142        }
143        updateRenderSize();
144    }
145
146    /**
147     * Tells the ScreenNail to override the default aspect ratio scaling
148     * and instead perform custom scaling to basically do a centerCrop instead
149     * of the default centerInside
150     *
151     * Note that calls to setSize will disable this
152     */
153    public void enableAspectRatioClamping() {
154        mEnableAspectRatioClamping = true;
155        updateRenderSize();
156    }
157
158    private void setPreviewLayoutSize(int w, int h) {
159        Log.i(TAG, "preview layout size: "+w+"/"+h);
160        mRenderWidth = w;
161        mRenderHeight = h;
162        updateRenderSize();
163    }
164
165    private void updateRenderSize() {
166        if (!mEnableAspectRatioClamping) {
167            mScaleX = mScaleY = 1f;
168            mUncroppedRenderWidth = getTextureWidth();
169            mUncroppedRenderHeight = getTextureHeight();
170            Log.i(TAG, "aspect ratio clamping disabled");
171            return;
172        }
173
174        float aspectRatio;
175        if (getTextureWidth() > getTextureHeight()) {
176            aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
177        } else {
178            aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
179        }
180        float scaledTextureWidth, scaledTextureHeight;
181        if (mRenderWidth > mRenderHeight) {
182            scaledTextureWidth = Math.max(mRenderWidth,
183                    (int) (mRenderHeight * aspectRatio));
184            scaledTextureHeight = Math.max(mRenderHeight,
185                    (int)(mRenderWidth / aspectRatio));
186        } else {
187            scaledTextureWidth = Math.max(mRenderWidth,
188                    (int) (mRenderHeight / aspectRatio));
189            scaledTextureHeight = Math.max(mRenderHeight,
190                    (int) (mRenderWidth * aspectRatio));
191        }
192        mScaleX = mRenderWidth / scaledTextureWidth;
193        mScaleY = mRenderHeight / scaledTextureHeight;
194        mUncroppedRenderWidth = Math.round(scaledTextureWidth);
195        mUncroppedRenderHeight = Math.round(scaledTextureHeight);
196        Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
197    }
198
199    @Override
200    public void acquireSurfaceTexture() {
201        synchronized (mLock) {
202            mFirstFrameArrived = false;
203            super.acquireSurfaceTexture();
204            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
205        }
206    }
207
208    @Override
209    public void releaseSurfaceTexture() {
210        synchronized (mLock) {
211            super.releaseSurfaceTexture();
212            mAnimState = ANIM_NONE; // stop the animation
213        }
214    }
215
216    public void copyTexture() {
217        synchronized (mLock) {
218            mListener.requestRender();
219            mAnimState = ANIM_SWITCH_COPY_TEXTURE;
220        }
221    }
222
223    public void animateSwitchCamera() {
224        Log.v(TAG, "animateSwitchCamera");
225        synchronized (mLock) {
226            if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
227                // Do not request render here because camera has been just
228                // started. We do not want to draw black frames.
229                mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
230            }
231        }
232    }
233
234    public void animateCapture(int displayRotation) {
235        synchronized (mLock) {
236            mCaptureAnimManager.setOrientation(displayRotation);
237            mCaptureAnimManager.animateFlashAndSlide();
238            mListener.requestRender();
239            mAnimState = ANIM_CAPTURE_START;
240        }
241    }
242
243    public void animateFlash(int displayRotation) {
244        synchronized (mLock) {
245            mCaptureAnimManager.setOrientation(displayRotation);
246            mCaptureAnimManager.animateFlash();
247            mListener.requestRender();
248            mAnimState = ANIM_CAPTURE_START;
249        }
250    }
251
252    public void animateSlide() {
253        synchronized (mLock) {
254            // Ignore the case where animateFlash is skipped but animateSlide is called
255            // e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back
256            // to camera. This case only happens in monkey tests, not applicable to normal
257            // human beings.
258            if (mAnimState != ANIM_CAPTURE_RUNNING) {
259                Log.v(TAG, "Cannot animateSlide outside of animateCapture!"
260                        + " Animation state = " + mAnimState);
261                return;
262            }
263            mCaptureAnimManager.animateSlide();
264            mListener.requestRender();
265        }
266    }
267
268    private void callbackIfNeeded() {
269        if (mOneTimeFrameDrawnListener != null) {
270            mOneTimeFrameDrawnListener.onFrameDrawn(this);
271            mOneTimeFrameDrawnListener = null;
272        }
273    }
274
275    @Override
276    protected void updateTransformMatrix(float[] matrix) {
277        super.updateTransformMatrix(matrix);
278        Matrix.translateM(matrix, 0, .5f, .5f, 0);
279        Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
280        Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
281    }
282
283    public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
284        super.draw(canvas, x, y, width, height);
285    }
286
287    @Override
288    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
289        synchronized (mLock) {
290            if (!mVisible) mVisible = true;
291            SurfaceTexture surfaceTexture = getSurfaceTexture();
292            if (surfaceTexture == null || !mFirstFrameArrived) return;
293
294            switch (mAnimState) {
295                case ANIM_NONE:
296                    super.draw(canvas, x, y, width, height);
297                    break;
298                case ANIM_SWITCH_COPY_TEXTURE:
299                    copyPreviewTexture(canvas);
300                    mSwitchAnimManager.setReviewDrawingSize(width, height);
301                    mListener.onPreviewTextureCopied();
302                    mAnimState = ANIM_SWITCH_DARK_PREVIEW;
303                    // The texture is ready. Fall through to draw darkened
304                    // preview.
305                case ANIM_SWITCH_DARK_PREVIEW:
306                case ANIM_SWITCH_WAITING_FIRST_FRAME:
307                    // Consume the frame. If the buffers are full,
308                    // onFrameAvailable will not be called. Animation state
309                    // relies on onFrameAvailable.
310                    surfaceTexture.updateTexImage();
311                    mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
312                            height, mAnimTexture);
313                    break;
314                case ANIM_SWITCH_START:
315                    mSwitchAnimManager.startAnimation();
316                    mAnimState = ANIM_SWITCH_RUNNING;
317                    break;
318                case ANIM_CAPTURE_START:
319                    copyPreviewTexture(canvas);
320                    mListener.onCaptureTextureCopied();
321                    mCaptureAnimManager.startAnimation(x, y, width, height);
322                    mAnimState = ANIM_CAPTURE_RUNNING;
323                    break;
324            }
325
326            if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
327                boolean drawn;
328                if (mAnimState == ANIM_CAPTURE_RUNNING) {
329                    if (!mFullScreen) {
330                        // Skip the animation if no longer in full screen mode
331                        drawn = false;
332                    } else {
333                        drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture);
334                    }
335                } else {
336                    drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
337                            width, height, this, mAnimTexture);
338                }
339                if (drawn) {
340                    mListener.requestRender();
341                } else {
342                    // Continue to the normal draw procedure if the animation is
343                    // not drawn.
344                    mAnimState = ANIM_NONE;
345                    super.draw(canvas, x, y, width, height);
346                }
347            }
348            callbackIfNeeded();
349        } // mLock
350    }
351
352    private void copyPreviewTexture(GLCanvas canvas) {
353        int width = mAnimTexture.getWidth();
354        int height = mAnimTexture.getHeight();
355        canvas.beginRenderTarget(mAnimTexture);
356        // Flip preview texture vertically. OpenGL uses bottom left point
357        // as the origin (0, 0).
358        canvas.translate(0, height);
359        canvas.scale(1, -1, 1);
360        getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
361        updateTransformMatrix(mTextureTransformMatrix);
362        canvas.drawTexture(mExtTexture,
363                mTextureTransformMatrix, 0, 0, width, height);
364        canvas.endRenderTarget();
365    }
366
367    @Override
368    public void noDraw() {
369        synchronized (mLock) {
370            mVisible = false;
371        }
372    }
373
374    @Override
375    public void recycle() {
376        synchronized (mLock) {
377            mVisible = false;
378        }
379    }
380
381    @Override
382    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
383        synchronized (mLock) {
384            if (getSurfaceTexture() != surfaceTexture) {
385                return;
386            }
387            mFirstFrameArrived = true;
388            if (mVisible) {
389                if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
390                    mAnimState = ANIM_SWITCH_START;
391                }
392                // We need to ask for re-render if the SurfaceTexture receives a new
393                // frame.
394                mListener.requestRender();
395            }
396        }
397    }
398
399    // We need to keep track of the size of preview frame on the screen because
400    // it's needed when we do switch-camera animation. See comments in
401    // SwitchAnimManager.java. This is based on the natural orientation, not the
402    // view system orientation.
403    public void setPreviewFrameLayoutSize(int width, int height) {
404        synchronized (mLock) {
405            mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
406            setPreviewLayoutSize(width, height);
407        }
408    }
409
410    public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
411        synchronized (mLock) {
412            mFirstFrameArrived = false;
413            mOneTimeFrameDrawnListener = l;
414        }
415    }
416}
417