diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index d174456f739d..b12ceb235168 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -955,7 +955,7 @@ public class BrowserApp extends GeckoApp if (enabled) { if (mLayerView != null) { - mLayerView.setOnMetricsChangedListener(this); + mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(this); } setToolbarMargin(0); mHomePagerContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0); @@ -963,7 +963,7 @@ public class BrowserApp extends GeckoApp // Immediately show the toolbar when disabling the dynamic // toolbar. if (mLayerView != null) { - mLayerView.setOnMetricsChangedListener(null); + mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(null); } mHomePagerContainer.setPadding(0, 0, 0, 0); if (mBrowserChrome != null) { diff --git a/mobile/android/base/GeckoEvent.java b/mobile/android/base/GeckoEvent.java index 146f4494527a..93bde7dec20d 100644 --- a/mobile/android/base/GeckoEvent.java +++ b/mobile/android/base/GeckoEvent.java @@ -105,7 +105,8 @@ public class GeckoEvent { TELEMETRY_UI_EVENT(44), GAMEPAD_ADDREMOVE(45), GAMEPAD_DATA(46), - LONG_PRESS(47); + LONG_PRESS(47), + ZOOMEDVIEW(48); public final int value; @@ -749,6 +750,17 @@ public class GeckoEvent { return event; } + public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) { + GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW); + event.mPoints = new Point[2]; + event.mPoints[0] = new Point(x, y); + event.mPoints[1] = new Point(bufw, bufh); + event.mX = (double) scaleFactor; + event.mMetaState = tabId; + event.mBuffer = buffer; + return event; + } + public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation) { GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED); event.mScreenOrientation = aScreenOrientation; diff --git a/mobile/android/base/ZoomedView.java b/mobile/android/base/ZoomedView.java new file mode 100644 index 000000000000..595c7f51a162 --- /dev/null +++ b/mobile/android/base/ZoomedView.java @@ -0,0 +1,490 @@ +package org.mozilla.gecko; + +import java.text.DecimalFormat; + +import java.nio.ByteBuffer; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.mozglue.DirectBufferAllocator; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.BitmapFactory; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChangedListener, + LayerView.OnZoomedViewListener, GeckoEventListener { + private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName(); + + private static final int ZOOM_FACTOR = 2; + private static final int W_CAPTURED_VIEW_IN_PERCENT = 80; + private static final int H_CAPTURED_VIEW_IN_PERCENT = 50; + private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000; + private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000; + + private ImageView zoomedImageView; + private LayerView layerView; + private MotionEvent actionDownEvent; + private int viewWidth; + private int viewHeight; + private int xLastPosition; + private int yLastPosition; + private boolean shouldSetVisibleOnUpdate; + private PointF convertedPosition; + private PointF returnValue; + + private boolean stopUpdateView; + + private int lastOrientation = 0; + + private ByteBuffer buffer; + private Runnable requestRenderRunnable; + private long startTimeReRender = 0; + private long lastStartTimeReRender = 0; + + private class ZoomedViewTouchListener implements View.OnTouchListener { + private float originRawX; + private float originRawY; + private int touchState; + + @Override + public boolean onTouch(View view, MotionEvent event) { + if (layerView == null) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_MOVE: + if (moveZoomedView(event)) { + touchState = MotionEvent.ACTION_MOVE; + } + break; + + case MotionEvent.ACTION_UP: + if (touchState == MotionEvent.ACTION_MOVE) { + touchState = -1; + } else { + layerView.dispatchTouchEvent(actionDownEvent); + actionDownEvent.recycle(); + convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); + MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), + MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y, + event.getMetaState()); + layerView.dispatchTouchEvent(e); + e.recycle(); + } + break; + + case MotionEvent.ACTION_DOWN: + touchState = -1; + originRawX = event.getRawX(); + originRawY = event.getRawY(); + convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY()); + actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), + MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y, + event.getMetaState()); + break; + } + return true; + } + + private boolean moveZoomedView(MotionEvent event) { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams(); + if ((touchState != MotionEvent.ACTION_MOVE) && (Math.abs((int) (event.getRawX() - originRawX)) < 1) + && (Math.abs((int) (event.getRawY() - originRawY)) < 1)) { + // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position. + // In this case, the move is ignored if the delta is lower than 1 unit. + return false; + } + + float newLeftMargin = params.leftMargin + event.getRawX() - originRawX; + float newTopMargin = params.topMargin + event.getRawY() - originRawY; + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin); + originRawX = event.getRawX(); + originRawY = event.getRawY(); + return true; + } + } + + public ZoomedView(Context context) { + this(context, null, 0); + } + + public ZoomedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZoomedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + convertedPosition = new PointF(); + returnValue = new PointF(); + requestRenderRunnable = new Runnable() { + @Override + public void run() { + requestZoomedViewRender(); + } + }; + EventDispatcher.getInstance().registerGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress", + "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange"); + } + + void destroy() { + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress", + "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange"); + } + + // This method (onFinishInflate) is called only when the zoomed view class is used inside + // an xml structure = parentHeight) { + newLayoutParams.topMargin = (int) (parentHeight - viewHeight); + } + + if (newLeftMargin < leftMarginMin) { + newLayoutParams.leftMargin = leftMarginMin; + } else if (newLeftMargin + viewWidth > parentWidth) { + newLayoutParams.leftMargin = (int) (parentWidth - viewWidth); + } + + setLayoutParams(newLayoutParams); + convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0); + xLastPosition = Math.round(convertedPosition.x); + yLastPosition = Math.round(convertedPosition.y); + requestZoomedViewRender(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // In case of orientation change, the zoomed view update is stopped until the orientation change + // is completed. At this time, the function onMetricsChanged is called and the + // zoomed view update is restarted again. + if (lastOrientation != newConfig.orientation) { + shouldBlockUpdate(true); + lastOrientation = newConfig.orientation; + } + } + + public void refreshZoomedViewSize(ImmutableViewportMetrics viewport) { + if (layerView == null) { + return; + } + + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams(); + setCapturedSize(viewport); + moveZoomedView(viewport, params.leftMargin, params.topMargin); + } + + public void setCapturedSize(ImmutableViewportMetrics metrics) { + if (layerView == null) { + return; + } + float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight()); + viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR; + viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR; + } + + public void shouldBlockUpdate(boolean shouldBlockUpdate) { + stopUpdateView = shouldBlockUpdate; + } + + public Bitmap.Config getBitmapConfig() { + return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + } + + public void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) { + if (layerView == null) { + layerView = aLayerView; + layerView.addOnZoomedViewListener(this); + layerView.setOnMetricsChangedZoomedViewportListener(this); + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + setCapturedSize(metrics); + } + startTimeReRender = 0; + shouldSetVisibleOnUpdate = true; + moveUsingGeckoPosition(leftFromGecko, topFromGecko); + } + + public void stopZoomDisplay() { + shouldSetVisibleOnUpdate = false; + this.setVisibility(View.GONE); + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + if (layerView != null) { + layerView.setOnMetricsChangedZoomedViewportListener(null); + layerView.removeOnZoomedViewListener(this); + layerView = null; + } + } + + @Override + public void handleMessage(final String event, final JSONObject message) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + try { + if (event.equals("Gesture:nothingDoneOnLongPress") || event.equals("Gesture:clusteredLinksClicked")) { + final JSONObject clickPosition = message.getJSONObject("clickPosition"); + int left = clickPosition.getInt("x"); + int top = clickPosition.getInt("y"); + // Start to display inside the zoomedView + LayerView geckoAppLayerView = GeckoAppShell.getLayerView(); + if (geckoAppLayerView != null) { + startZoomDisplay(geckoAppLayerView, left, top); + } + } else if (event.equals("Window:Resize")) { + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + refreshZoomedViewSize(metrics); + } else if (event.equals("Content:LocationChange")) { + stopZoomDisplay(); + } + } catch (JSONException e) { + Log.e(LOGTAG, "JSON exception", e); + } + } + }); + } + + private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) { + if (layerView == null) { + return; + } + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor), + (topFromGecko * metrics.zoomFactor)); + moveZoomedView(metrics, convertedPosition.x, convertedPosition.y); + } + + @Override + public void onMetricsChanged(final ImmutableViewportMetrics viewport) { + // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient). + // Post to UI Thread to avoid Exception: + // "Only the original thread that created a view hierarchy can touch its views." + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + if (layerView == null) { + return; + } + shouldBlockUpdate(false); + refreshZoomedViewSize(viewport); + } + }); + } + + @Override + public void onPanZoomStopped() { + } + + @Override + public void updateView(ByteBuffer data) { + final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig()); + if (sb3 != null) { + data.rewind(); + try { + sb3.copyPixelsFromBuffer(data); + } catch (Exception iae) { + Log.w(LOGTAG, iae.toString()); + } + BitmapDrawable ob3 = new BitmapDrawable(getResources(), sb3); + if (zoomedImageView != null) { + zoomedImageView.setImageDrawable(ob3); + } + } + if (shouldSetVisibleOnUpdate) { + this.setVisibility(View.VISIBLE); + shouldSetVisibleOnUpdate = false; + } + lastStartTimeReRender = startTimeReRender; + startTimeReRender = 0; + } + + private void updateBufferSize() { + int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; + int capacity = viewWidth * viewHeight * pixelSize; + if (buffer == null || buffer.capacity() != capacity) { + buffer = DirectBufferAllocator.free(buffer); + buffer = DirectBufferAllocator.allocate(capacity); + } + } + + private boolean isRendering() { + return (startTimeReRender != 0); + } + + private boolean renderFrequencyTooHigh() { + return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS); + } + + @Override + public void requestZoomedViewRender() { + if (stopUpdateView) { + return; + } + // remove pending runnable + ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable); + + // "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without + // any thing changed in the zoomed area (useless calls from the "zoomed area" point of view). + // "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity + // of the html on this area. + // To avoid to slow down the application, the 2 following cases are tested: + + // 1- Last render is still running, plan another render later. + if (isRendering()) { + // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later + // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. + // For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make + // the zoomed render on this last call. + ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); + return; + } + + // 2- Current render occurs too early, plan another render later. + if (renderFrequencyTooHigh()) { + // post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later + // We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done. + // For a page WITH animation/video, the animation/video can be stopped, and we need to make + // the zoomed render on this last call. + ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS); + return; + } + + startTimeReRender = System.nanoTime(); + // Allocate the buffer if it's the first call. + // Change the buffer size if it's not the right size. + updateBufferSize(); + + int tabId = Tabs.getInstance().getSelectedTab().getId(); + + ImmutableViewportMetrics metrics = layerView.getViewportMetrics(); + PointF origin = metrics.getOrigin(); + PointF offset = metrics.getMarginOffset(); + + final int xPos = (int) (origin.x - offset.x) + xLastPosition; + final int yPos = (int) (origin.y - offset.y) + yLastPosition; + + GeckoEvent e = GeckoEvent.createZoomedViewEvent(tabId, xPos, yPos, viewWidth, + viewHeight, (float) (2.0 * metrics.zoomFactor), buffer); + GeckoAppShell.sendEventToGecko(e); + } + +} diff --git a/mobile/android/base/gfx/GeckoLayerClient.java b/mobile/android/base/gfx/GeckoLayerClient.java index ba04952fff18..3becfceb9b19 100644 --- a/mobile/android/base/gfx/GeckoLayerClient.java +++ b/mobile/android/base/gfx/GeckoLayerClient.java @@ -86,7 +86,8 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget * that because mViewportMetrics might get reassigned in between reading the different * fields. */ private volatile ImmutableViewportMetrics mViewportMetrics; - private LayerView.OnMetricsChangedListener mViewportChangeListener; + private LayerView.OnMetricsChangedListener mDynamicToolbarViewportChangeListener; + private LayerView.OnMetricsChangedListener mZoomedViewViewportChangeListener; private ZoomConstraints mZoomConstraints; @@ -853,8 +854,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget * You must hold the monitor while calling this. */ private void viewportMetricsChanged(boolean notifyGecko) { - if (mViewportChangeListener != null) { - mViewportChangeListener.onMetricsChanged(mViewportMetrics); + if (mDynamicToolbarViewportChangeListener != null) { + mDynamicToolbarViewportChangeListener.onMetricsChanged(mViewportMetrics); + } + if (mZoomedViewViewportChangeListener != null) { + mZoomedViewViewportChangeListener.onMetricsChanged(mViewportMetrics); } mView.requestRender(); @@ -910,8 +914,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget /** Implementation of PanZoomTarget */ @Override public void panZoomStopped() { - if (mViewportChangeListener != null) { - mViewportChangeListener.onPanZoomStopped(); + if (mDynamicToolbarViewportChangeListener != null) { + mDynamicToolbarViewportChangeListener.onPanZoomStopped(); + } + if (mZoomedViewViewportChangeListener != null) { + mZoomedViewViewportChangeListener.onPanZoomStopped(); } } @@ -982,8 +989,12 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget return layerPoint; } - void setOnMetricsChangedListener(LayerView.OnMetricsChangedListener listener) { - mViewportChangeListener = listener; + void setOnMetricsChangedDynamicToolbarViewportListener(LayerView.OnMetricsChangedListener listener) { + mDynamicToolbarViewportChangeListener = listener; + } + + void setOnMetricsChangedZoomedViewportListener(LayerView.OnMetricsChangedListener listener) { + mZoomedViewViewportChangeListener = listener; } public void addDrawListener(DrawListener listener) { diff --git a/mobile/android/base/gfx/LayerRenderer.java b/mobile/android/base/gfx/LayerRenderer.java index 46c5071ff6f8..eb2e283bdc79 100644 --- a/mobile/android/base/gfx/LayerRenderer.java +++ b/mobile/android/base/gfx/LayerRenderer.java @@ -26,13 +26,17 @@ import android.graphics.RectF; import android.opengl.GLES20; import android.os.SystemClock; import android.util.Log; + import org.mozilla.gecko.mozglue.JNITarget; +import org.mozilla.gecko.util.ThreadUtils; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.ArrayList; +import java.util.List; import javax.microedition.khronos.egl.EGLConfig; @@ -55,6 +59,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { private static final long NANOS_PER_MS = 1000000; private static final int NANOS_PER_SECOND = 1000000000; + private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5; + private final LayerView mView; private final ScrollbarLayer mHorizScrollLayer; private final ScrollbarLayer mVertScrollLayer; @@ -90,6 +96,10 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { private int mSampleHandle; private int mTMatrixHandle; + private List mZoomedViewListeners; + private float mViewLeft = 0.0f; + private float mViewTop = 0.0f; + // column-major matrix applied to each vertex to shift the viewport from // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by // a factor of 2 to fill up the screen @@ -158,6 +168,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { mCoordBuffer = mCoordByteBuffer.asFloatBuffer(); Tabs.registerOnTabsChangedListener(this); + mZoomedViewListeners = new ArrayList(); } private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) { @@ -185,6 +196,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { mHorizScrollLayer.destroy(); mVertScrollLayer.destroy(); Tabs.unregisterOnTabsChangedListener(this); + mZoomedViewListeners.clear(); } void onSurfaceCreated(EGLConfig config) { @@ -586,6 +598,41 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { } + public void maybeRequestZoomedViewRender(RenderContext context){ + // Concurrently update of mZoomedViewListeners should not be an issue here + if (mZoomedViewListeners.size() == 0) { + return; + } + + // When scrolling fast, do not request zoomed view render to avoid to slow down + // the scroll in the main view. + // Speed is estimated using the offset changes between 2 display frame calls + final float viewLeft = context.viewport.left - context.offset.x; + final float viewTop = context.viewport.top - context.offset.y; + boolean shouldWaitToRender = false; + + if (Math.abs(mViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER || + Math.abs(mViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) { + shouldWaitToRender = true; + } + + mViewLeft = viewLeft; + mViewTop = viewTop; + + if (shouldWaitToRender) { + return; + } + + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) { + listener.requestZoomedViewRender(); + } + } + }); + } + /** This function is invoked via JNI; be careful when modifying signature. */ @JNITarget public void endDrawing() { @@ -595,6 +642,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { PanningPerfAPI.recordFrameTime(); + maybeRequestZoomedViewRender(mPageContext); + /* Used by robocop for testing purposes */ IntBuffer pixelBuffer = mPixelBuffer; if (mUpdated && pixelBuffer != null) { @@ -642,4 +691,25 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener { } } } + + public void updateZoomedView(final ByteBuffer data) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) { + listener.updateView(data); + } + } + }); + } + + public void addOnZoomedViewListener(LayerView.OnZoomedViewListener listener) { + ThreadUtils.assertOnUiThread(); + mZoomedViewListeners.add(listener); + } + + public void removeOnZoomedViewListener(LayerView.OnZoomedViewListener listener) { + ThreadUtils.assertOnUiThread(); + mZoomedViewListeners.remove(listener); + } } diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index 00347bd6ac4e..06c72f89bde3 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -5,7 +5,9 @@ package org.mozilla.gecko.gfx; +import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; import org.mozilla.gecko.AndroidGamepadManager; import org.mozilla.gecko.AppConstants.Versions; @@ -530,6 +532,19 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener } } + @WrapElementForJNI(allowMultithread = true, stubName = "updateZoomedView") + public static void updateZoomedView(ByteBuffer data) { + data.position(0); + LayerView layerView = GeckoAppShell.getLayerView(); + if (layerView != null) { + LayerRenderer layerRenderer = layerView.getRenderer(); + if (layerRenderer != null){ + layerRenderer.updateZoomedView(data); + } + } + return; + } + public interface Listener { void renderRequested(); void sizeChanged(int width, int height); @@ -662,7 +677,27 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener public void onPanZoomStopped(); } - public void setOnMetricsChangedListener(OnMetricsChangedListener listener) { - mLayerClient.setOnMetricsChangedListener(listener); + public void setOnMetricsChangedDynamicToolbarViewportListener(OnMetricsChangedListener listener) { + mLayerClient.setOnMetricsChangedDynamicToolbarViewportListener(listener); } + + public void setOnMetricsChangedZoomedViewportListener(OnMetricsChangedListener listener) { + mLayerClient.setOnMetricsChangedZoomedViewportListener(listener); + } + + // Public hooks for zoomed view + + public interface OnZoomedViewListener { + public void requestZoomedViewRender(); + public void updateView(ByteBuffer data); + } + + public void addOnZoomedViewListener(OnZoomedViewListener listener) { + mRenderer.addOnZoomedViewListener(listener); + } + + public void removeOnZoomedViewListener(OnZoomedViewListener listener) { + mRenderer.removeOnZoomedViewListener(listener); + } + } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 33a690437f74..acace83023a6 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -500,6 +500,7 @@ gbjar.sources += [ 'widget/ThumbnailView.java', 'widget/TwoWayView.java', 'ZoomConstraints.java', + 'ZoomedView.java', ] # The following sources are checked in to version control but # generated by a script (widget/generate_themed_views.py). If you're diff --git a/mobile/android/base/resources/layout/shared_ui_components.xml b/mobile/android/base/resources/layout/shared_ui_components.xml index eef98d80678a..512a408fb7d7 100644 --- a/mobile/android/base/resources/layout/shared_ui_components.xml +++ b/mobile/android/base/resources/layout/shared_ui_components.xml @@ -25,6 +25,7 @@ android:visibility="gone"/> + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/base/util/ThreadUtils.java b/mobile/android/base/util/ThreadUtils.java index 90043b589ee7..10a5942d0aa9 100644 --- a/mobile/android/base/util/ThreadUtils.java +++ b/mobile/android/base/util/ThreadUtils.java @@ -96,6 +96,14 @@ public final class ThreadUtils { sUiHandler.post(runnable); } + public static void postDelayedToUiThread(Runnable runnable, long timeout) { + sUiHandler.postDelayed(runnable, timeout); + } + + public static void removeCallbacksFromUiThread(Runnable runnable) { + sUiHandler.removeCallbacks(runnable); + } + public static Thread getBackgroundThread() { return sBackgroundThread; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 59cfd1d54c3d..5bcddc04279d 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -4980,7 +4980,12 @@ var BrowserEventHandler = { if (!target) { return; } + this._inCluster = aEvent.hitCluster; + if (this._inCluster) { + return; // No highlight for a cluster of links + } + let uri = this._getLinkURI(target); if (uri) { try { @@ -5097,6 +5102,7 @@ var BrowserEventHandler = { let data = JSON.parse(aData); let {x, y} = data; + if (this._inCluster) { this._clusterClicked(x, y); } else { @@ -5129,14 +5135,14 @@ var BrowserEventHandler = { } }, - _clusterClicked: function sh_clusterClicked(aX, aY) { - Messaging.sendRequest({ - type: "Gesture:clusteredLinksClicked", - clicPosition: { - x: aX, - y: aY - } - }); + _clusterClicked: function(aX, aY) { + Messaging.sendRequest({ + type: "Gesture:clusteredLinksClicked", + clickPosition: { + x: aX, + y: aY + } + }); }, onDoubleTap: function(aData) { diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp index 1116b1563f09..98d63e26fcff 100644 --- a/widget/android/AndroidBridge.cpp +++ b/widget/android/AndroidBridge.cpp @@ -1704,6 +1704,94 @@ AndroidBridge::GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId, return true; } +static float +GetScaleFactor(nsPresContext* mPresContext) { + nsIPresShell* presShell = mPresContext->PresShell(); + LayoutDeviceToLayerScale cumulativeResolution(presShell->GetCumulativeResolution().width); + return cumulativeResolution.scale; +} + +nsresult +AndroidBridge::CaptureZoomedView (nsIDOMWindow *window, nsIntRect zoomedViewRect, Object::Param buffer, + float zoomFactor) { + nsresult rv; + struct timeval timeEnd; + struct timeval timeEndAfter; + struct timeval timeStart; + struct timeval res; + gettimeofday (&timeStart, NULL); + + if (!buffer) + return NS_ERROR_FAILURE; + + nsCOMPtr < nsIDOMWindowUtils > utils = do_GetInterface (window); + if (!utils) + return NS_ERROR_FAILURE; + + JNIEnv* env = GetJNIEnv (); + + AutoLocalJNIFrame jniFrame (env, 0); + + nsCOMPtr < nsPIDOMWindow > win = do_QueryInterface (window); + if (!win) { + return NS_ERROR_FAILURE; + } + nsRefPtr < nsPresContext > presContext; + + nsIDocShell* docshell = win->GetDocShell (); + + if (docshell) { + docshell->GetPresContext (getter_AddRefs (presContext)); + } + + if (!presContext) { + return NS_ERROR_FAILURE; + } + nsCOMPtr < nsIPresShell > presShell = presContext->PresShell (); + + float scaleFactor = GetScaleFactor(presContext) ; + + nscolor bgColor = NS_RGB (255, 255, 255); + uint32_t renderDocFlags = (nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING | nsIPresShell::RENDER_DOCUMENT_RELATIVE); + nsRect r (presContext->DevPixelsToAppUnits(zoomedViewRect.x / scaleFactor), + presContext->DevPixelsToAppUnits(zoomedViewRect.y / scaleFactor ), + presContext->DevPixelsToAppUnits(zoomedViewRect.width / scaleFactor ), + presContext->DevPixelsToAppUnits(zoomedViewRect.height / scaleFactor )); + + bool is24bit = (GetScreenDepth () == 24); + SurfaceFormat format = is24bit ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::R5G6B5; + gfxImageFormat iFormat = gfx::SurfaceFormatToImageFormat(format); + uint32_t stride = gfxASurface::FormatStrideForWidth(iFormat, zoomedViewRect.width); + + uint8_t* data = static_cast (env->GetDirectBufferAddress (buffer.Get())); + if (!data) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT (gfxPlatform::GetPlatform ()->SupportsAzureContentForType (BackendType::CAIRO), + "Need BackendType::CAIRO support"); + RefPtr < DrawTarget > dt = Factory::CreateDrawTargetForData ( + BackendType::CAIRO, data, IntSize (zoomedViewRect.width, zoomedViewRect.height), stride, + format); + if (!dt) { + ALOG_BRIDGE ("Error creating DrawTarget"); + return NS_ERROR_FAILURE; + } + nsRefPtr < gfxContext > context = new gfxContext (dt); + context->SetMatrix (context->CurrentMatrix ().Scale(zoomFactor, zoomFactor)); + + rv = presShell->RenderDocument (r, renderDocFlags, bgColor, context); + + if (is24bit) { + gfxUtils::ConvertBGRAtoRGBA (data, stride * zoomedViewRect.height); + } + + LayerView::updateZoomedView(buffer); + + NS_ENSURE_SUCCESS (rv, rv); + return NS_OK; +} + nsresult AndroidBridge::CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, Object::Param buffer, bool &shouldStore) { nsresult rv; diff --git a/widget/android/AndroidBridge.h b/widget/android/AndroidBridge.h index 563a1e3186e8..5e22bdb1f244 100644 --- a/widget/android/AndroidBridge.h +++ b/widget/android/AndroidBridge.h @@ -188,6 +188,7 @@ public: bool GetThreadNameJavaProfiling(uint32_t aThreadId, nsCString & aResult); bool GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId, uint32_t aFrameId, nsCString & aResult); + nsresult CaptureZoomedView(nsIDOMWindow *window, nsIntRect zoomedViewRect, jni::Object::Param buffer, float zoomFactor); nsresult CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, jni::Object::Param buffer, bool &shouldStore); void GetDisplayPort(bool aPageSizeUpdate, bool aIsBrowserContentDisplayed, int32_t tabId, nsIAndroidViewport* metrics, nsIAndroidDisplayport** displayPort); void ContentDocumentChanged(); diff --git a/widget/android/AndroidJavaWrappers.cpp b/widget/android/AndroidJavaWrappers.cpp index 5e61c62c2a8d..838567fcfbd7 100644 --- a/widget/android/AndroidJavaWrappers.cpp +++ b/widget/android/AndroidJavaWrappers.cpp @@ -538,6 +538,14 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj) break; } + case ZOOMEDVIEW: { + mX = jenv->GetDoubleField(jobj, jXField); + mMetaState = jenv->GetIntField(jobj, jMetaStateField); + ReadPointArray(mPoints, jenv, jPoints, 2); + mByteBuffer = new RefCountedJavaObject(jenv, jenv->GetObjectField(jobj, jByteBufferField)); + break; + } + case SCREENORIENTATION_CHANGED: { mScreenOrientation = jenv->GetShortField(jobj, jScreenOrientationField); break; diff --git a/widget/android/AndroidJavaWrappers.h b/widget/android/AndroidJavaWrappers.h index 7bab4816580a..48f12f0d3557 100644 --- a/widget/android/AndroidJavaWrappers.h +++ b/widget/android/AndroidJavaWrappers.h @@ -746,6 +746,7 @@ public: GAMEPAD_ADDREMOVE = 45, GAMEPAD_DATA = 46, LONG_PRESS = 47, + ZOOMEDVIEW = 48, dummy_java_enum_list_end }; diff --git a/widget/android/GeneratedJNIWrappers.cpp b/widget/android/GeneratedJNIWrappers.cpp index 16b66f5fa9c7..235e8e350bf2 100644 --- a/widget/android/GeneratedJNIWrappers.cpp +++ b/widget/android/GeneratedJNIWrappers.cpp @@ -980,6 +980,14 @@ mozilla::jni::Object::LocalRef LayerView::RegisterCompositorWrapper() return mozilla::jni::Method::Call(nullptr, nullptr); } +constexpr char LayerView::updateZoomedView_t::name[]; +constexpr char LayerView::updateZoomedView_t::signature[]; + +void LayerView::updateZoomedView(mozilla::jni::Object::Param a0) +{ + return mozilla::jni::Method::Call(nullptr, nullptr, a0); +} + constexpr char NativePanZoomController::name[]; constexpr char NativePanZoomController::RequestContentRepaintWrapper_t::name[]; diff --git a/widget/android/GeneratedJNIWrappers.h b/widget/android/GeneratedJNIWrappers.h index 182857ad9944..98edf0c22095 100644 --- a/widget/android/GeneratedJNIWrappers.h +++ b/widget/android/GeneratedJNIWrappers.h @@ -1934,6 +1934,21 @@ public: static mozilla::jni::Object::LocalRef RegisterCompositorWrapper(); +public: + struct updateZoomedView_t { + typedef LayerView Owner; + typedef void ReturnType; + typedef void SetterType; + static constexpr char name[] = "updateZoomedView"; + static constexpr char signature[] = + "(Ljava/nio/ByteBuffer;)V"; + static const bool isStatic = true; + static const bool isMultithreaded = true; + static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT; + }; + + static void updateZoomedView(mozilla::jni::Object::Param); + }; class NativePanZoomController : public mozilla::jni::Class { diff --git a/widget/android/nsAppShell.cpp b/widget/android/nsAppShell.cpp index dd8c20dd1f42..3076e88e507e 100644 --- a/widget/android/nsAppShell.cpp +++ b/widget/android/nsAppShell.cpp @@ -393,6 +393,33 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait) break; } + case AndroidGeckoEvent::ZOOMEDVIEW: { + if (!mBrowserApp) + break; + int32_t tabId = curEvent->MetaState(); + const nsTArray& points = curEvent->Points(); + float scaleFactor = (float) curEvent->X(); + nsRefPtr javaBuffer = curEvent->ByteBuffer(); + const auto& mBuffer = jni::Object::Ref::From(javaBuffer->GetObject()); + + nsCOMPtr domWindow; + nsCOMPtr tab; + mBrowserApp->GetBrowserTab(tabId, getter_AddRefs(tab)); + if (!tab) { + NS_ERROR("Can't find tab!"); + break; + } + tab->GetWindow(getter_AddRefs(domWindow)); + if (!domWindow) { + NS_ERROR("Can't find dom window!"); + break; + } + NS_ASSERTION(points.Length() == 2, "ZoomedView event does not have enough coordinates"); + nsIntRect r(points[0].x, points[0].y, points[1].x, points[1].y); + nsresult rv = AndroidBridge::Bridge()->CaptureZoomedView(domWindow, r, mBuffer, scaleFactor); + break; + } + case AndroidGeckoEvent::VIEWPORT: case AndroidGeckoEvent::BROADCAST: { if (curEvent->Characters().Length() == 0)