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