1/*
2 * Copyright (C) 2010 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.gallery3d.ui;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.graphics.Matrix;
22import android.graphics.PixelFormat;
23import android.opengl.GLSurfaceView;
24import android.os.Build;
25import android.os.Process;
26import android.os.SystemClock;
27import android.util.AttributeSet;
28import android.view.MotionEvent;
29import android.view.SurfaceHolder;
30import android.view.View;
31
32import com.android.gallery3d.R;
33import com.android.gallery3d.anim.CanvasAnimation;
34import com.android.gallery3d.common.ApiHelper;
35import com.android.gallery3d.common.Utils;
36import com.android.gallery3d.glrenderer.BasicTexture;
37import com.android.gallery3d.glrenderer.GLCanvas;
38import com.android.gallery3d.glrenderer.GLES11Canvas;
39import com.android.gallery3d.glrenderer.GLES20Canvas;
40import com.android.gallery3d.glrenderer.UploadedTexture;
41import com.android.gallery3d.util.GalleryUtils;
42import com.android.gallery3d.util.MotionEventHelper;
43import com.android.gallery3d.util.Profile;
44
45import java.util.ArrayDeque;
46import java.util.ArrayList;
47import java.util.concurrent.locks.Condition;
48import java.util.concurrent.locks.ReentrantLock;
49
50import javax.microedition.khronos.egl.EGLConfig;
51import javax.microedition.khronos.opengles.GL10;
52import javax.microedition.khronos.opengles.GL11;
53
54// The root component of all <code>GLView</code>s. The rendering is done in GL
55// thread while the event handling is done in the main thread.  To synchronize
56// the two threads, the entry points of this package need to synchronize on the
57// <code>GLRootView</code> instance unless it can be proved that the rendering
58// thread won't access the same thing as the method. The entry points include:
59// (1) The public methods of HeadUpDisplay
60// (2) The public methods of CameraHeadUpDisplay
61// (3) The overridden methods in GLRootView.
62public class GLRootView extends GLSurfaceView
63        implements GLSurfaceView.Renderer, GLRoot {
64    private static final String TAG = "GLRootView";
65
66    private static final boolean DEBUG_FPS = false;
67    private int mFrameCount = 0;
68    private long mFrameCountingStart = 0;
69
70    private static final boolean DEBUG_INVALIDATE = false;
71    private int mInvalidateColor = 0;
72
73    private static final boolean DEBUG_DRAWING_STAT = false;
74
75    private static final boolean DEBUG_PROFILE = false;
76    private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
77
78    private static final int FLAG_INITIALIZED = 1;
79    private static final int FLAG_NEED_LAYOUT = 2;
80
81    private GL11 mGL;
82    private GLCanvas mCanvas;
83    private GLView mContentView;
84
85    private OrientationSource mOrientationSource;
86    // mCompensation is the difference between the UI orientation on GLCanvas
87    // and the framework orientation. See OrientationManager for details.
88    private int mCompensation;
89    // mCompensationMatrix maps the coordinates of touch events. It is kept sync
90    // with mCompensation.
91    private Matrix mCompensationMatrix = new Matrix();
92    private int mDisplayRotation;
93
94    private int mFlags = FLAG_NEED_LAYOUT;
95    private volatile boolean mRenderRequested = false;
96
97    private final ArrayList<CanvasAnimation> mAnimations =
98            new ArrayList<CanvasAnimation>();
99
100    private final ArrayDeque<OnGLIdleListener> mIdleListeners =
101            new ArrayDeque<OnGLIdleListener>();
102
103    private final IdleRunner mIdleRunner = new IdleRunner();
104
105    private final ReentrantLock mRenderLock = new ReentrantLock();
106    private final Condition mFreezeCondition =
107            mRenderLock.newCondition();
108    private boolean mFreeze;
109
110    private long mLastDrawFinishTime;
111    private boolean mInDownState = false;
112    private boolean mFirstDraw = true;
113
114    public GLRootView(Context context) {
115        this(context, null);
116    }
117
118    public GLRootView(Context context, AttributeSet attrs) {
119        super(context, attrs);
120        mFlags |= FLAG_INITIALIZED;
121        setBackgroundDrawable(null);
122        setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1);
123        if (ApiHelper.USE_888_PIXEL_FORMAT) {
124            setEGLConfigChooser(8, 8, 8, 0, 0, 0);
125        } else {
126            setEGLConfigChooser(5, 6, 5, 0, 0, 0);
127        }
128        setRenderer(this);
129        if (ApiHelper.USE_888_PIXEL_FORMAT) {
130            getHolder().setFormat(PixelFormat.RGB_888);
131        } else {
132            getHolder().setFormat(PixelFormat.RGB_565);
133        }
134
135        // Uncomment this to enable gl error check.
136        // setDebugFlags(DEBUG_CHECK_GL_ERROR);
137    }
138
139    @Override
140    public void registerLaunchedAnimation(CanvasAnimation animation) {
141        // Register the newly launched animation so that we can set the start
142        // time more precisely. (Usually, it takes much longer for first
143        // rendering, so we set the animation start time as the time we
144        // complete rendering)
145        mAnimations.add(animation);
146    }
147
148    @Override
149    public void addOnGLIdleListener(OnGLIdleListener listener) {
150        synchronized (mIdleListeners) {
151            mIdleListeners.addLast(listener);
152            mIdleRunner.enable();
153        }
154    }
155
156    @Override
157    public void setContentPane(GLView content) {
158        if (mContentView == content) return;
159        if (mContentView != null) {
160            if (mInDownState) {
161                long now = SystemClock.uptimeMillis();
162                MotionEvent cancelEvent = MotionEvent.obtain(
163                        now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
164                mContentView.dispatchTouchEvent(cancelEvent);
165                cancelEvent.recycle();
166                mInDownState = false;
167            }
168            mContentView.detachFromRoot();
169            BasicTexture.yieldAllTextures();
170        }
171        mContentView = content;
172        if (content != null) {
173            content.attachToRoot(this);
174            requestLayoutContentPane();
175        }
176    }
177
178    @Override
179    public void requestRenderForced() {
180        superRequestRender();
181    }
182
183    @Override
184    public void requestRender() {
185        if (DEBUG_INVALIDATE) {
186            StackTraceElement e = Thread.currentThread().getStackTrace()[4];
187            String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
188            Log.d(TAG, "invalidate: " + caller);
189        }
190        if (mRenderRequested) return;
191        mRenderRequested = true;
192        if (ApiHelper.HAS_POST_ON_ANIMATION) {
193            postOnAnimation(mRequestRenderOnAnimationFrame);
194        } else {
195            super.requestRender();
196        }
197    }
198
199    private Runnable mRequestRenderOnAnimationFrame = new Runnable() {
200        @Override
201        public void run() {
202            superRequestRender();
203        }
204    };
205
206    private void superRequestRender() {
207        super.requestRender();
208    }
209
210    @Override
211    public void requestLayoutContentPane() {
212        mRenderLock.lock();
213        try {
214            if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
215
216            // "View" system will invoke onLayout() for initialization(bug ?), we
217            // have to ignore it since the GLThread is not ready yet.
218            if ((mFlags & FLAG_INITIALIZED) == 0) return;
219
220            mFlags |= FLAG_NEED_LAYOUT;
221            requestRender();
222        } finally {
223            mRenderLock.unlock();
224        }
225    }
226
227    private void layoutContentPane() {
228        mFlags &= ~FLAG_NEED_LAYOUT;
229
230        int w = getWidth();
231        int h = getHeight();
232        int displayRotation = 0;
233        int compensation = 0;
234
235        // Get the new orientation values
236        if (mOrientationSource != null) {
237            displayRotation = mOrientationSource.getDisplayRotation();
238            compensation = mOrientationSource.getCompensation();
239        } else {
240            displayRotation = 0;
241            compensation = 0;
242        }
243
244        if (mCompensation != compensation) {
245            mCompensation = compensation;
246            if (mCompensation % 180 != 0) {
247                mCompensationMatrix.setRotate(mCompensation);
248                // move center to origin before rotation
249                mCompensationMatrix.preTranslate(-w / 2, -h / 2);
250                // align with the new origin after rotation
251                mCompensationMatrix.postTranslate(h / 2, w / 2);
252            } else {
253                mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
254            }
255        }
256        mDisplayRotation = displayRotation;
257
258        // Do the actual layout.
259        if (mCompensation % 180 != 0) {
260            int tmp = w;
261            w = h;
262            h = tmp;
263        }
264        Log.i(TAG, "layout content pane " + w + "x" + h
265                + " (compensation " + mCompensation + ")");
266        if (mContentView != null && w != 0 && h != 0) {
267            mContentView.layout(0, 0, w, h);
268        }
269        // Uncomment this to dump the view hierarchy.
270        //mContentView.dumpTree("");
271    }
272
273    @Override
274    protected void onLayout(
275            boolean changed, int left, int top, int right, int bottom) {
276        if (changed) requestLayoutContentPane();
277    }
278
279    /**
280     * Called when the context is created, possibly after automatic destruction.
281     */
282    // This is a GLSurfaceView.Renderer callback
283    @Override
284    public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
285        GL11 gl = (GL11) gl1;
286        if (mGL != null) {
287            // The GL Object has changed
288            Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
289        }
290        mRenderLock.lock();
291        try {
292            mGL = gl;
293            mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl);
294            BasicTexture.invalidateAllTextures();
295        } finally {
296            mRenderLock.unlock();
297        }
298
299        if (DEBUG_FPS || DEBUG_PROFILE) {
300            setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
301        } else {
302            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
303        }
304    }
305
306    /**
307     * Called when the OpenGL surface is recreated without destroying the
308     * context.
309     */
310    // This is a GLSurfaceView.Renderer callback
311    @Override
312    public void onSurfaceChanged(GL10 gl1, int width, int height) {
313        Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
314                + ", gl10: " + gl1.toString());
315        Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
316        GalleryUtils.setRenderThread();
317        if (DEBUG_PROFILE) {
318            Log.d(TAG, "Start profiling");
319            Profile.enable(20);  // take a sample every 20ms
320        }
321        GL11 gl = (GL11) gl1;
322        Utils.assertTrue(mGL == gl);
323
324        mCanvas.setSize(width, height);
325    }
326
327    private void outputFps() {
328        long now = System.nanoTime();
329        if (mFrameCountingStart == 0) {
330            mFrameCountingStart = now;
331        } else if ((now - mFrameCountingStart) > 1000000000) {
332            Log.d(TAG, "fps: " + (double) mFrameCount
333                    * 1000000000 / (now - mFrameCountingStart));
334            mFrameCountingStart = now;
335            mFrameCount = 0;
336        }
337        ++mFrameCount;
338    }
339
340    @Override
341    public void onDrawFrame(GL10 gl) {
342        AnimationTime.update();
343        long t0;
344        if (DEBUG_PROFILE_SLOW_ONLY) {
345            Profile.hold();
346            t0 = System.nanoTime();
347        }
348        mRenderLock.lock();
349
350        while (mFreeze) {
351            mFreezeCondition.awaitUninterruptibly();
352        }
353
354        try {
355            onDrawFrameLocked(gl);
356        } finally {
357            mRenderLock.unlock();
358        }
359
360        // We put a black cover View in front of the SurfaceView and hide it
361        // after the first draw. This prevents the SurfaceView being transparent
362        // before the first draw.
363        if (mFirstDraw) {
364            mFirstDraw = false;
365            post(new Runnable() {
366                    @Override
367                    public void run() {
368                        View root = getRootView();
369                        View cover = root.findViewById(R.id.gl_root_cover);
370                        cover.setVisibility(GONE);
371                    }
372                });
373        }
374
375        if (DEBUG_PROFILE_SLOW_ONLY) {
376            long t = System.nanoTime();
377            long durationInMs = (t - mLastDrawFinishTime) / 1000000;
378            long durationDrawInMs = (t - t0) / 1000000;
379            mLastDrawFinishTime = t;
380
381            if (durationInMs > 34) {  // 34ms -> we skipped at least 2 frames
382                Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
383                        durationInMs + ") -----");
384                Profile.commit();
385            } else {
386                Profile.drop();
387            }
388        }
389    }
390
391    private void onDrawFrameLocked(GL10 gl) {
392        if (DEBUG_FPS) outputFps();
393
394        // release the unbound textures and deleted buffers.
395        mCanvas.deleteRecycledResources();
396
397        // reset texture upload limit
398        UploadedTexture.resetUploadLimit();
399
400        mRenderRequested = false;
401
402        if ((mOrientationSource != null
403                && mDisplayRotation != mOrientationSource.getDisplayRotation())
404                || (mFlags & FLAG_NEED_LAYOUT) != 0) {
405            layoutContentPane();
406        }
407
408        mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
409        rotateCanvas(-mCompensation);
410        if (mContentView != null) {
411           mContentView.render(mCanvas);
412        } else {
413            // Make sure we always draw something to prevent displaying garbage
414            mCanvas.clearBuffer();
415        }
416        mCanvas.restore();
417
418        if (!mAnimations.isEmpty()) {
419            long now = AnimationTime.get();
420            for (int i = 0, n = mAnimations.size(); i < n; i++) {
421                mAnimations.get(i).setStartTime(now);
422            }
423            mAnimations.clear();
424        }
425
426        if (UploadedTexture.uploadLimitReached()) {
427            requestRender();
428        }
429
430        synchronized (mIdleListeners) {
431            if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
432        }
433
434        if (DEBUG_INVALIDATE) {
435            mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
436            mInvalidateColor = ~mInvalidateColor;
437        }
438
439        if (DEBUG_DRAWING_STAT) {
440            mCanvas.dumpStatisticsAndClear();
441        }
442    }
443
444    private void rotateCanvas(int degrees) {
445        if (degrees == 0) return;
446        int w = getWidth();
447        int h = getHeight();
448        int cx = w / 2;
449        int cy = h / 2;
450        mCanvas.translate(cx, cy);
451        mCanvas.rotate(degrees, 0, 0, 1);
452        if (degrees % 180 != 0) {
453            mCanvas.translate(-cy, -cx);
454        } else {
455            mCanvas.translate(-cx, -cy);
456        }
457    }
458
459    @Override
460    public boolean dispatchTouchEvent(MotionEvent event) {
461        if (!isEnabled()) return false;
462
463        int action = event.getAction();
464        if (action == MotionEvent.ACTION_CANCEL
465                || action == MotionEvent.ACTION_UP) {
466            mInDownState = false;
467        } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
468            return false;
469        }
470
471        if (mCompensation != 0) {
472            event = MotionEventHelper.transformEvent(event, mCompensationMatrix);
473        }
474
475        mRenderLock.lock();
476        try {
477            // If this has been detached from root, we don't need to handle event
478            boolean handled = mContentView != null
479                    && mContentView.dispatchTouchEvent(event);
480            if (action == MotionEvent.ACTION_DOWN && handled) {
481                mInDownState = true;
482            }
483            return handled;
484        } finally {
485            mRenderLock.unlock();
486        }
487    }
488
489    private class IdleRunner implements Runnable {
490        // true if the idle runner is in the queue
491        private boolean mActive = false;
492
493        @Override
494        public void run() {
495            OnGLIdleListener listener;
496            synchronized (mIdleListeners) {
497                mActive = false;
498                if (mIdleListeners.isEmpty()) return;
499                listener = mIdleListeners.removeFirst();
500            }
501            mRenderLock.lock();
502            boolean keepInQueue;
503            try {
504                keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);
505            } finally {
506                mRenderLock.unlock();
507            }
508            synchronized (mIdleListeners) {
509                if (keepInQueue) mIdleListeners.addLast(listener);
510                if (!mRenderRequested && !mIdleListeners.isEmpty()) enable();
511            }
512        }
513
514        public void enable() {
515            // Who gets the flag can add it to the queue
516            if (mActive) return;
517            mActive = true;
518            queueEvent(this);
519        }
520    }
521
522    @Override
523    public void lockRenderThread() {
524        mRenderLock.lock();
525    }
526
527    @Override
528    public void unlockRenderThread() {
529        mRenderLock.unlock();
530    }
531
532    @Override
533    public void onPause() {
534        unfreeze();
535        super.onPause();
536        if (DEBUG_PROFILE) {
537            Log.d(TAG, "Stop profiling");
538            Profile.disableAll();
539            Profile.dumpToFile("/sdcard/gallery.prof");
540            Profile.reset();
541        }
542    }
543
544    @Override
545    public void setOrientationSource(OrientationSource source) {
546        mOrientationSource = source;
547    }
548
549    @Override
550    public int getDisplayRotation() {
551        return mDisplayRotation;
552    }
553
554    @Override
555    public int getCompensation() {
556        return mCompensation;
557    }
558
559    @Override
560    public Matrix getCompensationMatrix() {
561        return mCompensationMatrix;
562    }
563
564    @Override
565    public void freeze() {
566        mRenderLock.lock();
567        mFreeze = true;
568        mRenderLock.unlock();
569    }
570
571    @Override
572    public void unfreeze() {
573        mRenderLock.lock();
574        mFreeze = false;
575        mFreezeCondition.signalAll();
576        mRenderLock.unlock();
577    }
578
579    @Override
580    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
581    public void setLightsOutMode(boolean enabled) {
582        if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
583
584        int flags = 0;
585        if (enabled) {
586            flags = STATUS_BAR_HIDDEN;
587            if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
588                flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
589            }
590        }
591        setSystemUiVisibility(flags);
592    }
593
594    // We need to unfreeze in the following methods and in onPause().
595    // These methods will wait on GLThread. If we have freezed the GLRootView,
596    // the GLThread will wait on main thread to call unfreeze and cause dead
597    // lock.
598    @Override
599    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
600        unfreeze();
601        super.surfaceChanged(holder, format, w, h);
602    }
603
604    @Override
605    public void surfaceCreated(SurfaceHolder holder) {
606        unfreeze();
607        super.surfaceCreated(holder);
608    }
609
610    @Override
611    public void surfaceDestroyed(SurfaceHolder holder) {
612        unfreeze();
613        super.surfaceDestroyed(holder);
614    }
615
616    @Override
617    protected void onDetachedFromWindow() {
618        unfreeze();
619        super.onDetachedFromWindow();
620    }
621
622    @Override
623    protected void finalize() throws Throwable {
624        try {
625            unfreeze();
626        } finally {
627            super.finalize();
628        }
629    }
630}
631