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