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.glrenderer.GLCanvas;
26import com.android.gallery3d.glrenderer.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    private boolean mAcquireTexture = false;
81    private final DrawClient mDefaultDraw = new DrawClient() {
82        @Override
83        public void onDraw(GLCanvas canvas, int x, int y, int width, int height) {
84            CameraScreenNail.super.draw(canvas, x, y, width, height);
85        }
86
87        @Override
88        public boolean requiresSurfaceTexture() {
89            return true;
90        }
91    };
92    private DrawClient mDraw = mDefaultDraw;
93
94    public interface Listener {
95        void requestRender();
96        // Preview has been copied to a texture.
97        void onPreviewTextureCopied();
98
99        void onCaptureTextureCopied();
100    }
101
102    public interface OnFrameDrawnListener {
103        void onFrameDrawn(CameraScreenNail c);
104    }
105
106    public interface DrawClient {
107        void onDraw(GLCanvas canvas, int x, int y, int width, int height);
108
109        boolean requiresSurfaceTexture();
110    }
111
112    public CameraScreenNail(Listener listener) {
113        mListener = listener;
114    }
115
116    public void setFullScreen(boolean full) {
117        synchronized (mLock) {
118            mFullScreen = full;
119        }
120    }
121
122    /**
123     * returns the uncropped, but scaled, width of the rendered texture
124     */
125    public int getUncroppedRenderWidth() {
126        return mUncroppedRenderWidth;
127    }
128
129    /**
130     * returns the uncropped, but scaled, width of the rendered texture
131     */
132    public int getUncroppedRenderHeight() {
133        return mUncroppedRenderHeight;
134    }
135
136    @Override
137    public int getWidth() {
138        return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
139    }
140
141    @Override
142    public int getHeight() {
143        return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
144    }
145
146    private int getTextureWidth() {
147        return super.getWidth();
148    }
149
150    private int getTextureHeight() {
151        return super.getHeight();
152    }
153
154    @Override
155    public void setSize(int w, int h) {
156        super.setSize(w,  h);
157        mEnableAspectRatioClamping = false;
158        if (mRenderWidth == 0) {
159            mRenderWidth = w;
160            mRenderHeight = h;
161        }
162        updateRenderSize();
163    }
164
165    /**
166     * Tells the ScreenNail to override the default aspect ratio scaling
167     * and instead perform custom scaling to basically do a centerCrop instead
168     * of the default centerInside
169     *
170     * Note that calls to setSize will disable this
171     */
172    public void enableAspectRatioClamping() {
173        mEnableAspectRatioClamping = true;
174        updateRenderSize();
175    }
176
177    private void setPreviewLayoutSize(int w, int h) {
178        Log.i(TAG, "preview layout size: "+w+"/"+h);
179        mRenderWidth = w;
180        mRenderHeight = h;
181        updateRenderSize();
182    }
183
184    private void updateRenderSize() {
185        if (!mEnableAspectRatioClamping) {
186            mScaleX = mScaleY = 1f;
187            mUncroppedRenderWidth = getTextureWidth();
188            mUncroppedRenderHeight = getTextureHeight();
189            Log.i(TAG, "aspect ratio clamping disabled");
190            return;
191        }
192
193        float aspectRatio;
194        if (getTextureWidth() > getTextureHeight()) {
195            aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
196        } else {
197            aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
198        }
199        float scaledTextureWidth, scaledTextureHeight;
200        if (mRenderWidth > mRenderHeight) {
201            scaledTextureWidth = Math.max(mRenderWidth,
202                    (int) (mRenderHeight * aspectRatio));
203            scaledTextureHeight = Math.max(mRenderHeight,
204                    (int)(mRenderWidth / aspectRatio));
205        } else {
206            scaledTextureWidth = Math.max(mRenderWidth,
207                    (int) (mRenderHeight / aspectRatio));
208            scaledTextureHeight = Math.max(mRenderHeight,
209                    (int) (mRenderWidth * aspectRatio));
210        }
211        mScaleX = mRenderWidth / scaledTextureWidth;
212        mScaleY = mRenderHeight / scaledTextureHeight;
213        mUncroppedRenderWidth = Math.round(scaledTextureWidth);
214        mUncroppedRenderHeight = Math.round(scaledTextureHeight);
215        Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
216    }
217
218    public void acquireSurfaceTexture() {
219        synchronized (mLock) {
220            mFirstFrameArrived = false;
221            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
222            mAcquireTexture = true;
223        }
224        mListener.requestRender();
225    }
226
227    @Override
228    public void releaseSurfaceTexture() {
229        synchronized (mLock) {
230            if (mAcquireTexture) {
231                mAcquireTexture = false;
232                mLock.notifyAll();
233            } else {
234                if (super.getSurfaceTexture() != null) {
235                    super.releaseSurfaceTexture();
236                }
237                mAnimState = ANIM_NONE; // stop the animation
238            }
239        }
240    }
241
242    public void copyTexture() {
243        synchronized (mLock) {
244            mListener.requestRender();
245            mAnimState = ANIM_SWITCH_COPY_TEXTURE;
246        }
247    }
248
249    public void animateSwitchCamera() {
250        Log.v(TAG, "animateSwitchCamera");
251        synchronized (mLock) {
252            if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
253                // Do not request render here because camera has been just
254                // started. We do not want to draw black frames.
255                mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
256            }
257        }
258    }
259
260    public void animateCapture(int displayRotation) {
261        synchronized (mLock) {
262            mCaptureAnimManager.setOrientation(displayRotation);
263            mCaptureAnimManager.animateFlashAndSlide();
264            mListener.requestRender();
265            mAnimState = ANIM_CAPTURE_START;
266        }
267    }
268
269    public RawTexture getAnimationTexture() {
270        return mAnimTexture;
271    }
272
273    public void animateFlash(int displayRotation) {
274        synchronized (mLock) {
275            mCaptureAnimManager.setOrientation(displayRotation);
276            mCaptureAnimManager.animateFlash();
277            mListener.requestRender();
278            mAnimState = ANIM_CAPTURE_START;
279        }
280    }
281
282    public void animateSlide() {
283        synchronized (mLock) {
284            // Ignore the case where animateFlash is skipped but animateSlide is called
285            // e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back
286            // to camera. This case only happens in monkey tests, not applicable to normal
287            // human beings.
288            if (mAnimState != ANIM_CAPTURE_RUNNING) {
289                Log.v(TAG, "Cannot animateSlide outside of animateCapture!"
290                        + " Animation state = " + mAnimState);
291                return;
292            }
293            mCaptureAnimManager.animateSlide();
294            mListener.requestRender();
295        }
296    }
297
298    private void callbackIfNeeded() {
299        if (mOneTimeFrameDrawnListener != null) {
300            mOneTimeFrameDrawnListener.onFrameDrawn(this);
301            mOneTimeFrameDrawnListener = null;
302        }
303    }
304
305    @Override
306    protected void updateTransformMatrix(float[] matrix) {
307        super.updateTransformMatrix(matrix);
308        Matrix.translateM(matrix, 0, .5f, .5f, 0);
309        Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
310        Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
311    }
312
313    public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
314        DrawClient draw;
315        synchronized (mLock) {
316            draw = mDraw;
317        }
318        draw.onDraw(canvas, x, y, width, height);
319    }
320
321    public void setDraw(DrawClient draw) {
322        synchronized (mLock) {
323            if (draw == null) {
324                mDraw = mDefaultDraw;
325            } else {
326                mDraw = draw;
327            }
328        }
329        mListener.requestRender();
330    }
331
332    @Override
333    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
334        synchronized (mLock) {
335            allocateTextureIfRequested(canvas);
336            if (!mVisible) mVisible = true;
337            SurfaceTexture surfaceTexture = getSurfaceTexture();
338            if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) {
339                return;
340            }
341
342            switch (mAnimState) {
343                case ANIM_NONE:
344                    directDraw(canvas, x, y, width, height);
345                    break;
346                case ANIM_SWITCH_COPY_TEXTURE:
347                    copyPreviewTexture(canvas);
348                    mSwitchAnimManager.setReviewDrawingSize(width, height);
349                    mListener.onPreviewTextureCopied();
350                    mAnimState = ANIM_SWITCH_DARK_PREVIEW;
351                    // The texture is ready. Fall through to draw darkened
352                    // preview.
353                case ANIM_SWITCH_DARK_PREVIEW:
354                case ANIM_SWITCH_WAITING_FIRST_FRAME:
355                    // Consume the frame. If the buffers are full,
356                    // onFrameAvailable will not be called. Animation state
357                    // relies on onFrameAvailable.
358                    surfaceTexture.updateTexImage();
359                    mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
360                            height, mAnimTexture);
361                    break;
362                case ANIM_SWITCH_START:
363                    mSwitchAnimManager.startAnimation();
364                    mAnimState = ANIM_SWITCH_RUNNING;
365                    break;
366                case ANIM_CAPTURE_START:
367                    copyPreviewTexture(canvas);
368                    mListener.onCaptureTextureCopied();
369                    mCaptureAnimManager.startAnimation(x, y, width, height);
370                    mAnimState = ANIM_CAPTURE_RUNNING;
371                    break;
372            }
373
374            if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
375                boolean drawn;
376                if (mAnimState == ANIM_CAPTURE_RUNNING) {
377                    if (!mFullScreen) {
378                        // Skip the animation if no longer in full screen mode
379                        drawn = false;
380                    } else {
381                        drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture);
382                    }
383                } else {
384                    drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
385                            width, height, this, mAnimTexture);
386                }
387                if (drawn) {
388                    mListener.requestRender();
389                } else {
390                    // Continue to the normal draw procedure if the animation is
391                    // not drawn.
392                    mAnimState = ANIM_NONE;
393                    directDraw(canvas, x, y, width, height);
394                }
395            }
396            callbackIfNeeded();
397        } // mLock
398    }
399
400    private void copyPreviewTexture(GLCanvas canvas) {
401        if (!mDraw.requiresSurfaceTexture() && mAnimTexture == null) {
402            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
403            mAnimTexture.setIsFlippedVertically(true);
404        }
405        int width = mAnimTexture.getWidth();
406        int height = mAnimTexture.getHeight();
407        canvas.beginRenderTarget(mAnimTexture);
408        if (!mDraw.requiresSurfaceTexture()) {
409            mDraw.onDraw(canvas, 0, 0, width, height);
410        } else {
411            // Flip preview texture vertically. OpenGL uses bottom left point
412            // as the origin (0, 0).
413            canvas.translate(0, height);
414            canvas.scale(1, -1, 1);
415            getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
416            updateTransformMatrix(mTextureTransformMatrix);
417            canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height);
418        }
419        canvas.endRenderTarget();
420    }
421
422    @Override
423    public void noDraw() {
424        synchronized (mLock) {
425            mVisible = false;
426        }
427    }
428
429    @Override
430    public void recycle() {
431        synchronized (mLock) {
432            mVisible = false;
433        }
434    }
435
436    @Override
437    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
438        synchronized (mLock) {
439            if (getSurfaceTexture() != surfaceTexture) {
440                return;
441            }
442            mFirstFrameArrived = true;
443            if (mVisible) {
444                if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
445                    mAnimState = ANIM_SWITCH_START;
446                }
447                // We need to ask for re-render if the SurfaceTexture receives a new
448                // frame.
449                mListener.requestRender();
450            }
451        }
452    }
453
454    // We need to keep track of the size of preview frame on the screen because
455    // it's needed when we do switch-camera animation. See comments in
456    // SwitchAnimManager.java. This is based on the natural orientation, not the
457    // view system orientation.
458    public void setPreviewFrameLayoutSize(int width, int height) {
459        synchronized (mLock) {
460            mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
461            setPreviewLayoutSize(width, height);
462        }
463    }
464
465    public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
466        synchronized (mLock) {
467            mFirstFrameArrived = false;
468            mOneTimeFrameDrawnListener = l;
469        }
470    }
471
472    @Override
473    public SurfaceTexture getSurfaceTexture() {
474        synchronized (mLock) {
475            SurfaceTexture surfaceTexture = super.getSurfaceTexture();
476            if (surfaceTexture == null && mAcquireTexture) {
477                try {
478                    mLock.wait();
479                    surfaceTexture = super.getSurfaceTexture();
480                } catch (InterruptedException e) {
481                    Log.w(TAG, "unexpected interruption");
482                }
483            }
484            return surfaceTexture;
485        }
486    }
487
488    private void allocateTextureIfRequested(GLCanvas canvas) {
489        synchronized (mLock) {
490            if (mAcquireTexture) {
491                super.acquireSurfaceTexture(canvas);
492                mAcquireTexture = false;
493                mLock.notifyAll();
494            }
495        }
496    }
497}
498