/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.gfx; import org.mozilla.gecko.GeckoAccessibility; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.R; import org.mozilla.gecko.TouchEventInterceptor; import org.mozilla.gecko.ZoomConstraints; import org.mozilla.gecko.util.EventDispatcher; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.os.Build; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.FrameLayout; import java.nio.IntBuffer; import java.util.ArrayList; /** * A view rendered by the layer compositor. * * Note that LayerView is accessed by Robocop via reflection. */ public class LayerView extends FrameLayout { private static String LOGTAG = "GeckoLayerView"; private GeckoLayerClient mLayerClient; private PanZoomController mPanZoomController; private LayerMarginsAnimator mMarginsAnimator; private GLController mGLController; private InputConnectionHandler mInputConnectionHandler; private LayerRenderer mRenderer; /* Must be a PAINT_xxx constant */ private int mPaintState; private int mBackgroundColor; private boolean mFullScreen; private SurfaceView mSurfaceView; private TextureView mTextureView; private Listener mListener; /* This should only be modified on the Java UI thread. */ private final ArrayList mTouchInterceptors; /* Flags used to determine when to show the painted surface. */ public static final int PAINT_START = 0; public static final int PAINT_BEFORE_FIRST = 1; public static final int PAINT_AFTER_FIRST = 2; public boolean shouldUseTextureView() { // Disable TextureView support for now as it causes panning/zooming // performance regressions (see bug 792259). Uncomment the code below // once this bug is fixed. return false; /* // we can only use TextureView on ICS or higher if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Log.i(LOGTAG, "Not using TextureView: not on ICS+"); return false; } try { // and then we can only use it if we have a hardware accelerated window Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null); return (Boolean) m.invoke(this); } catch (Exception e) { Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString()); return false; } */ } public LayerView(Context context, AttributeSet attrs) { super(context, attrs); mGLController = GLController.getInstance(this); mPaintState = PAINT_START; mBackgroundColor = Color.WHITE; mTouchInterceptors = new ArrayList(); } public void initializeView(EventDispatcher eventDispatcher) { mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher); mPanZoomController = mLayerClient.getPanZoomController(); mMarginsAnimator = mLayerClient.getLayerMarginsAnimator(); mRenderer = new LayerRenderer(this); mInputConnectionHandler = null; setFocusable(true); setFocusableInTouchMode(true); GeckoAccessibility.setDelegate(this); } public void geckoConnected() { mLayerClient.notifyGeckoReady(); addTouchInterceptor(new TouchEventInterceptor() { private PointF mInitialTouchPoint = null; @Override public boolean onInterceptTouchEvent(View view, MotionEvent event) { return false; } @Override public boolean onTouch(View view, MotionEvent event) { if (event == null) { return true; } int action = event.getActionMasked(); PointF point = new PointF(event.getX(), event.getY()); if (action == MotionEvent.ACTION_DOWN) { mInitialTouchPoint = point; } if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) { if (PointUtils.subtract(point, mInitialTouchPoint).length() < PanZoomController.PAN_THRESHOLD) { // Don't send the touchmove event if if the users finger hasn't moved far. // Necessary for Google Maps to work correctly. See bug 771099. return true; } else { mInitialTouchPoint = null; } } GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false)); return true; } }); } public void show() { // Fix this if TextureView support is turned back on above mSurfaceView.setVisibility(View.VISIBLE); } public void hide() { // Fix this if TextureView support is turned back on above mSurfaceView.setVisibility(View.INVISIBLE); } public void destroy() { if (mLayerClient != null) { mLayerClient.destroy(); } if (mRenderer != null) { mRenderer.destroy(); } } public void addTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) { post(new Runnable() { @Override public void run() { mTouchInterceptors.add(aTouchInterceptor); } }); } public void removeTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) { post(new Runnable() { @Override public void run() { mTouchInterceptors.remove(aTouchInterceptor); } }); } private boolean runTouchInterceptors(MotionEvent event, boolean aOnTouch) { boolean result = false; for (TouchEventInterceptor i : mTouchInterceptors) { if (aOnTouch) { result |= i.onTouch(this, event); } else { result |= i.onInterceptTouchEvent(this, event); } } return result; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { requestFocus(); } if (runTouchInterceptors(event, false)) { return true; } if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) { return true; } if (runTouchInterceptors(event, true)) { return true; } return false; } @Override public boolean onHoverEvent(MotionEvent event) { if (runTouchInterceptors(event, true)) { return true; } return false; } @Override public boolean onGenericMotionEvent(MotionEvent event) { if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) { return true; } return false; } @Override protected void onAttachedToWindow() { // This check should not be done before the view is attached to a window // as hardware acceleration will not be enabled at that point. // We must create and add the SurfaceView instance before the view tree // is fully created to avoid flickering (see bug 801477). if (shouldUseTextureView()) { mTextureView = new TextureView(getContext()); mTextureView.setSurfaceTextureListener(new SurfaceTextureListener()); // The background is set to this color when the LayerView is // created, and it will be shown immediately at startup. Shortly // after, the tab's background color will be used before any content // is shown. mTextureView.setBackgroundResource(R.color.background_normal); addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } else { // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap) // from a SurfaceView, which is just not possible (the bitmap will be transparent). setWillNotCacheDrawing(false); mSurfaceView = new LayerSurfaceView(getContext(), this); mSurfaceView.setBackgroundResource(R.color.background_normal); addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); SurfaceHolder holder = mSurfaceView.getHolder(); holder.addCallback(new SurfaceListener()); holder.setFormat(PixelFormat.RGB_565); } } public GeckoLayerClient getLayerClient() { return mLayerClient; } public PanZoomController getPanZoomController() { return mPanZoomController; } public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; } public ImmutableViewportMetrics getViewportMetrics() { return mLayerClient.getViewportMetrics(); } public void abortPanning() { if (mPanZoomController != null) { mPanZoomController.abortPanning(); } } public PointF convertViewPointToLayerPoint(PointF viewPoint) { return mLayerClient.convertViewPointToLayerPoint(viewPoint); } int getBackgroundColor() { return mBackgroundColor; } @Override public void setBackgroundColor(int newColor) { mBackgroundColor = newColor; requestRender(); } public void setZoomConstraints(ZoomConstraints constraints) { mLayerClient.setZoomConstraints(constraints); } public void setIsRTL(boolean aIsRTL) { mLayerClient.setIsRTL(aIsRTL); } public void setInputConnectionHandler(InputConnectionHandler inputConnectionHandler) { mInputConnectionHandler = inputConnectionHandler; mLayerClient.forceRedraw(null); } @Override public Handler getHandler() { if (mInputConnectionHandler != null) return mInputConnectionHandler.getHandler(super.getHandler()); return super.getHandler(); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (mInputConnectionHandler != null) return mInputConnectionHandler.onCreateInputConnection(outAttrs); return null; } @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyPreIme(keyCode, event)) { return true; } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mPanZoomController != null && mPanZoomController.onKeyEvent(event)) { return true; } if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyDown(keyCode, event)) { return true; } return false; } @Override public boolean onKeyLongPress(int keyCode, KeyEvent event) { if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyLongPress(keyCode, event)) { return true; } return false; } @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event)) { return true; } return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyUp(keyCode, event)) { return true; } return false; } public boolean isIMEEnabled() { if (mInputConnectionHandler != null) { return mInputConnectionHandler.isIMEEnabled(); } return false; } public void requestRender() { if (mListener != null) { mListener.renderRequested(); } } public void addLayer(Layer layer) { mRenderer.addLayer(layer); } public void removeLayer(Layer layer) { mRenderer.removeLayer(layer); } public int getMaxTextureSize() { return mRenderer.getMaxTextureSize(); } /** Used by robocop for testing purposes. Not for production use! */ public IntBuffer getPixels() { return mRenderer.getPixels(); } /* paintState must be a PAINT_xxx constant. */ public void setPaintState(int paintState) { mPaintState = paintState; } public int getPaintState() { return mPaintState; } public LayerRenderer getRenderer() { return mRenderer; } public void setListener(Listener listener) { mListener = listener; } Listener getListener() { return mListener; } public GLController getGLController() { return mGLController; } private Bitmap getDrawable(int resId) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapUtils.decodeResource(getContext(), resId, options); } Bitmap getShadowPattern() { return getDrawable(R.drawable.shadow); } Bitmap getScrollbarImage() { return getDrawable(R.drawable.scrollbar); } /* When using a SurfaceView (mSurfaceView != null), resizing happens in two * phases. First, the LayerView changes size, then, often some frames later, * the SurfaceView changes size. Because of this, we need to split the * resize into two phases to avoid jittering. * * The first phase is the LayerView size change. mListener is notified so * that a synchronous draw can be performed (otherwise a blank frame will * appear). * * The second phase is the SurfaceView size change. At this point, the * backing GL surface is resized and another synchronous draw is performed. * Gecko is also sent the new window size, and this will likely cause an * extra draw a few frames later, after it's re-rendered and caught up. * * In the case that there is no valid GL surface (for example, when * resuming, or when coming back from the awesomescreen), or we're using a * TextureView instead of a SurfaceView, the first phase is skipped. */ private void onSizeChanged(int width, int height) { if (!mGLController.hasValidSurface() || mSurfaceView == null) { surfaceChanged(width, height); return; } if (mListener != null) { mListener.sizeChanged(width, height); } } private void surfaceChanged(int width, int height) { mGLController.surfaceChanged(width, height); if (mListener != null) { mListener.surfaceChanged(width, height); } } private void onDestroyed() { mGLController.surfaceDestroyed(); } public Object getNativeWindow() { if (mSurfaceView != null) return mSurfaceView.getHolder(); return mTextureView.getSurfaceTexture(); } /** This function is invoked by Gecko (compositor thread) via JNI; be careful when modifying signature. */ public static GLController registerCxxCompositor() { try { LayerView layerView = GeckoAppShell.getLayerView(); GLController controller = layerView.getGLController(); controller.compositorCreated(); return controller; } catch (Exception e) { Log.e(LOGTAG, "Error registering compositor!", e); return null; } } public interface Listener { void renderRequested(); void sizeChanged(int width, int height); void surfaceChanged(int width, int height); } private class SurfaceListener implements SurfaceHolder.Callback { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { onSizeChanged(width, height); } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { onDestroyed(); } } /* A subclass of SurfaceView to listen to layout changes, as * View.OnLayoutChangeListener requires API level 11. */ private class LayerSurfaceView extends SurfaceView { LayerView mParent; public LayerSurfaceView(Context aContext, LayerView aParent) { super(aContext); mParent = aParent; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (changed) { mParent.surfaceChanged(right - left, bottom - top); } } } private class SurfaceTextureListener implements TextureView.SurfaceTextureListener { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged, // but that is not the case here. onSizeChanged(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { onDestroyed(); return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { onSizeChanged(width, height); } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } } @Override public void setOverScrollMode(int overscrollMode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { super.setOverScrollMode(overscrollMode); } if (mPanZoomController != null) { mPanZoomController.setOverScrollMode(overscrollMode); } } @Override public int getOverScrollMode() { if (mPanZoomController != null) { return mPanZoomController.getOverScrollMode(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { return super.getOverScrollMode(); } return View.OVER_SCROLL_ALWAYS; } @Override public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus); } public void setFullScreen(boolean fullScreen) { mFullScreen = fullScreen; } public boolean isFullScreen() { return mFullScreen; } }