Bug 663803 - Zoomed view implementation using render document r=mcomella,snorp
authordominique vincent <domivinc@toitl.com>
Wed, 21 Jan 2015 05:59:23 +0100
changeset 239438 32ce7927011cf27e80253b3c93a1999038a1a793
parent 239437 a7736ab45ab468d4483f82b9e8215900cac444cb
child 239439 43847e260e1fd561dca8b8066a0d28c9423c6f10
push id497
push usermleibovic@mozilla.com
push dateWed, 28 Jan 2015 16:43:37 +0000
reviewersmcomella, snorp
bugs663803
milestone38.0a1
Bug 663803 - Zoomed view implementation using render document r=mcomella,snorp
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoEvent.java
mobile/android/base/ZoomedView.java
mobile/android/base/gfx/GeckoLayerClient.java
mobile/android/base/gfx/LayerRenderer.java
mobile/android/base/gfx/LayerView.java
mobile/android/base/moz.build
mobile/android/base/resources/layout/shared_ui_components.xml
mobile/android/base/resources/layout/zoomed_view.xml
mobile/android/base/util/ThreadUtils.java
mobile/android/chrome/content/browser.js
widget/android/AndroidBridge.cpp
widget/android/AndroidBridge.h
widget/android/AndroidJavaWrappers.cpp
widget/android/AndroidJavaWrappers.h
widget/android/GeneratedJNIWrappers.cpp
widget/android/GeneratedJNIWrappers.h
widget/android/nsAppShell.cpp
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -950,25 +950,25 @@ public class BrowserApp extends GeckoApp
         ps.show("", "", items, ListView.CHOICE_MODE_NONE);
     }
 
     private void setDynamicToolbarEnabled(boolean enabled) {
         ThreadUtils.assertOnUiThread();
 
         if (enabled) {
             if (mLayerView != null) {
-                mLayerView.setOnMetricsChangedListener(this);
+                mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(this);
             }
             setToolbarMargin(0);
             mHomePagerContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0);
         } else {
             // 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) {
                 ViewHelper.setTranslationY(mBrowserChrome, 0);
             }
         }
 
         refreshToolbarHeight();
--- a/mobile/android/base/GeckoEvent.java
+++ b/mobile/android/base/GeckoEvent.java
@@ -100,17 +100,18 @@ public class GeckoEvent {
         PREFERENCES_OBSERVE(39),
         PREFERENCES_GET(40),
         PREFERENCES_REMOVE_OBSERVERS(41),
         TELEMETRY_UI_SESSION_START(42),
         TELEMETRY_UI_SESSION_STOP(43),
         TELEMETRY_UI_EVENT(44),
         GAMEPAD_ADDREMOVE(45),
         GAMEPAD_DATA(46),
-        LONG_PRESS(47);
+        LONG_PRESS(47),
+        ZOOMEDVIEW(48);
 
         public final int value;
 
         private NativeGeckoEvent(int value) {
             this.value = value;
         }
     }
 
@@ -744,16 +745,27 @@ public class GeckoEvent {
         GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.THUMBNAIL);
         event.mPoints = new Point[1];
         event.mPoints[0] = new Point(bufw, bufh);
         event.mMetaState = tabId;
         event.mBuffer = buffer;
         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;
         return event;
     }
 
     public static GeckoEvent createCallObserverEvent(String observerKey, String topic, String data) {
         GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.CALL_OBSERVER);
new file mode 100644
--- /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 <org.mozilla.gecko.ZoomedView ...
+    // It won't be called if the class is used from java code like "new  ZoomedView(context);"
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        ImageView closeButton = (ImageView) findViewById(R.id.dialog_close);
+        closeButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                stopZoomDisplay();
+            }
+        });
+
+        zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
+        zoomedImageView.setOnTouchListener(new ZoomedViewTouchListener());
+    }
+
+    /*
+     * Convert a click from ZoomedView. Return the position of the click in the
+     * LayerView
+     */
+    private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
+        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        PointF offset = metrics.getMarginOffset();
+        final float parentWidth = metrics.getWidth();
+        final float parentHeight = metrics.getHeight();
+        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
+
+        returnValue.x = (int) ((x / ZOOM_FACTOR) +     // Conversion of the x offset inside the zoomed view (using the scale factor)
+
+                        offset.x +               // The offset of the layerView
+
+                        /* Conversion of the left side position of the zoomed view
+                         *   Minimum value for the left side of the zoomed view is 0
+                         *     and we return 0 after conversion
+                         *   Maximum value for the left side of the zoomed view is (parentWidth - offset.x - viewWidth)
+                         *     and we return (parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) after conversion.
+                         */
+                        (((float) params.leftMargin) - offset.x) *
+                            ((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
+                            (parentWidth - offset.x - viewWidth)));
+
+        // Same comments here vertically
+        returnValue.y = (int) ((y / ZOOM_FACTOR) +
+                        offset.y +
+                        (((float) params.topMargin) - offset.y) *
+                            ((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
+                            (parentHeight - offset.y - viewHeight)));
+
+        return returnValue;
+    }
+
+    /*
+     * A touch point (x,y) occurs in LayerView, this point should be displayed
+     * in the center of the zoomed view. The returned point is the position of
+     * the Top-Left zoomed view point on the screen device
+     */
+    private PointF getZoomedViewTopLeftPositionFromTouchPosition(float x, float y) {
+        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        PointF offset = metrics.getMarginOffset();
+        final float parentWidth = metrics.getWidth();
+        final float parentHeight = metrics.getHeight();
+
+        returnValue.x = (int) ((((x - (viewWidth / (2 * ZOOM_FACTOR)))) /   // Translation to get the left side position of the zoomed view
+                                                                        // centered on x (the value 2 to get the middle).
+
+                        /* Conversion of the left side position of the zoomed view.
+                         * See the comment in getUnzoomedPositionFromPointInZoomedView.
+                         * The proportional factor is the same. It is used in a division
+                         * and not in a multiplication to convert the position from
+                         * the LayerView to the ZoomedView.
+                         */
+                        ((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
+                        (parentWidth - offset.x - viewWidth)))
+
+                + offset.x);     // The offset of the layerView
+
+        // Same comments here vertically
+        returnValue.y = (int) ((((y - (viewHeight / (2 * ZOOM_FACTOR)))) /
+                        ((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
+                        (parentHeight - offset.y - viewHeight)))
+                + offset.y);
+
+        return returnValue;
+    }
+
+    private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin) {
+        if (layerView == null) {
+            return;
+        }
+
+        final float parentWidth = metrics.getWidth();
+        final float parentHeight = metrics.getHeight();
+        RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
+        newLayoutParams.leftMargin = (int) newLeftMargin;
+        newLayoutParams.topMargin = (int) newTopMargin;
+        int topMarginMin;
+        int leftMarginMin;
+        PointF offset = metrics.getMarginOffset();
+        topMarginMin = (int) offset.y;
+        leftMarginMin = (int) offset.x;
+
+        if (newTopMargin < topMarginMin) {
+            newLayoutParams.topMargin = topMarginMin;
+        } else if (newTopMargin + viewHeight >= 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);
+    }
+
+}
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -81,17 +81,18 @@ class GeckoLayerClient implements LayerV
      * Specifically:
      * 1) reading mViewportMetrics from any thread is fine without synchronization
      * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
      * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
      *    case 1 above) you should always first grab a local copy of the reference, and then use
      *    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;
 
     private boolean mGeckoIsReady;
 
     private final PanZoomController mPanZoomController;
     private final LayerMarginsAnimator mMarginsAnimator;
     private final LayerView mView;
@@ -848,18 +849,21 @@ class GeckoLayerClient implements LayerV
 
         viewportMetricsChanged(notifyGecko);
     }
 
     /*
      * 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();
         if (notifyGecko && mGeckoIsReady) {
             geometryChanged(null);
         }
     }
 
@@ -905,18 +909,21 @@ class GeckoLayerClient implements LayerV
             mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy);
         mViewportMetrics = mViewportMetrics.setMarginsFrom(newMarginsMetrics);
         viewportMetricsChanged(true);
     }
 
     /** Implementation of PanZoomTarget */
     @Override
     public void panZoomStopped() {
-        if (mViewportChangeListener != null) {
-            mViewportChangeListener.onPanZoomStopped();
+        if (mDynamicToolbarViewportChangeListener != null) {
+        	mDynamicToolbarViewportChangeListener.onPanZoomStopped();
+        }
+        if (mZoomedViewViewportChangeListener != null) {
+        	mZoomedViewViewportChangeListener.onPanZoomStopped();
         }
     }
 
     /** Implementation of PanZoomTarget */
     @Override
     public void forceRedraw(DisplayPortMetrics displayPort) {
         mForceRedraw = true;
         if (mGeckoIsReady) {
@@ -977,18 +984,22 @@ class GeckoLayerClient implements LayerV
         // the current Gecko coordinate in CSS pixels.
         PointF layerPoint = new PointF(
                 ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
                 ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
 
         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) {
         mDrawListeners.add(listener);
     }
 
     public void removeDrawListener(DrawListener listener) {
         mDrawListeners.remove(listener);
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -21,23 +21,27 @@ import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
 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;
 
 /**
  * The layer renderer implements the rendering logic for a layer view.
  */
 public class LayerRenderer implements Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "GeckoLayerRenderer";
@@ -50,16 +54,18 @@ public class LayerRenderer implements Ta
     private static final int MAX_FRAME_TIME = 16;   /* 1000 ms / 60 FPS */
 
     private static final int FRAME_RATE_METER_WIDTH = 128;
     private static final int FRAME_RATE_METER_HEIGHT = 32;
 
     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;
     private final FadeRunnable mFadeRunnable;
     private ByteBuffer mCoordByteBuffer;
     private FloatBuffer mCoordBuffer;
     private RenderContext mLastPageContext;
     private int mMaxTextureSize;
@@ -85,16 +91,20 @@ public class LayerRenderer implements Ta
 
     // Used by GLES 2.0
     private int mProgram;
     private int mPositionHandle;
     private int mTextureHandle;
     private int mSampleHandle;
     private int mTMatrixHandle;
 
+    private List<LayerView.OnZoomedViewListener> 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
     public static final float[] DEFAULT_TEXTURE_MATRIX = {
         2.0f, 0.0f, 0.0f, 0.0f,
         0.0f, 2.0f, 0.0f, 0.0f,
         0.0f, 0.0f, 2.0f, 0.0f,
         -1.0f, -1.0f, 0.0f, 1.0f
@@ -153,16 +163,17 @@ public class LayerRenderer implements Ta
 
         // Initialize the FloatBuffer that will be used to store all vertices and texture
         // coordinates in draw() commands.
         mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4);
         mCoordByteBuffer.order(ByteOrder.nativeOrder());
         mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
 
         Tabs.registerOnTabsChangedListener(this);
+        mZoomedViewListeners = new ArrayList<LayerView.OnZoomedViewListener>();
     }
 
     private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) {
         IntSize potSize = size.nextPowerOfTwo();
         if (size.equals(potSize)) {
             return image;
         }
         // make the bitmap size a power-of-two in both dimensions if it's not already.
@@ -180,16 +191,17 @@ public class LayerRenderer implements Ta
 
     public void destroy() {
         DirectBufferAllocator.free(mCoordByteBuffer);
         mCoordByteBuffer = null;
         mCoordBuffer = null;
         mHorizScrollLayer.destroy();
         mVertScrollLayer.destroy();
         Tabs.unregisterOnTabsChangedListener(this);
+        mZoomedViewListeners.clear();
     }
 
     void onSurfaceCreated(EGLConfig config) {
         checkMonitoringEnabled();
         createDefaultProgram();
         activateDefaultProgram();
     }
 
@@ -581,25 +593,62 @@ public class LayerRenderer implements Ta
                     printCheckerboardStats();
                 }
             }
 
             runRenderTasks(mTasks, true, mFrameStartTime);
 
         }
 
+        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() {
             // If a layer update requires further work, schedule another redraw
             if (!mUpdated)
                 mView.requestRender();
 
             PanningPerfAPI.recordFrameTime();
 
+            maybeRequestZoomedViewRender(mPageContext);
+
             /* Used by robocop for testing purposes */
             IntBuffer pixelBuffer = mPixelBuffer;
             if (mUpdated && pixelBuffer != null) {
                 synchronized (pixelBuffer) {
                     pixelBuffer.position(0);
                     GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(),
                                         (int)mScreenContext.viewport.height(), GLES20.GL_RGBA,
                                         GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
@@ -637,9 +686,30 @@ public class LayerRenderer implements Ta
 
                 if (mView.getChildAt(0) != null) {
                     mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor());
                 }
                 mView.setPaintState(LayerView.PAINT_START);
             }
         }
     }
+
+    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);
+    }
 }
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -1,16 +1,18 @@
 /* -*- 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 java.nio.ByteBuffer;
 import java.nio.IntBuffer;
+import java.util.ArrayList;
 
 import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAccessibility;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.PrefsHelper;
@@ -525,16 +527,29 @@ public class LayerView extends FrameLayo
             controller.compositorCreated();
             return controller;
         } catch (Exception e) {
             Log.e(LOGTAG, "Error registering compositor!", e);
             return null;
         }
     }
 
+    @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);
         void surfaceChanged(int width, int height);
     }
 
     private class SurfaceListener implements SurfaceHolder.Callback {
         @Override
@@ -657,12 +672,32 @@ public class LayerView extends FrameLayo
 
     // Public hooks for listening to metrics changing
 
     public interface OnMetricsChangedListener {
         public void onMetricsChanged(ImmutableViewportMetrics viewport);
         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);
+    }
+
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -495,16 +495,17 @@ gbjar.sources += [
     'widget/IconTabWidget.java',
     'widget/ResizablePathDrawable.java',
     'widget/SquaredImageView.java',
     'widget/SwipeDismissListViewTouchListener.java',
     'widget/TabThumbnailWrapper.java',
     '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
 # editing this list, make sure to edit that script.
 gbjar.sources += [
     'widget/ThemedEditText.java',
     'widget/ThemedImageButton.java',
     'widget/ThemedImageView.java',
--- a/mobile/android/base/resources/layout/shared_ui_components.xml
+++ b/mobile/android/base/resources/layout/shared_ui_components.xml
@@ -20,16 +20,17 @@
                     android:layout_height="match_parent"/>
 
     <org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
                                        android:layout_width="match_parent"
                                        android:layout_height="match_parent"
                                        android:visibility="gone"/>
 
     <include layout="@layout/text_selection_handles"/>
+    <include layout="@layout/zoomed_view"/>
 
     <FrameLayout android:id="@+id/camera_layout"
                  android:layout_height="wrap_content"
                  android:layout_width="wrap_content"
                  android:layout_alignParentRight="true"
                  android:layout_alignParentBottom="true">
     </FrameLayout>
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/zoomed_view.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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/.
+-->
+
+<org.mozilla.gecko.ZoomedView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:gecko="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/zoomed_view_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentLeft="true"
+    android:layout_alignParentTop="true"
+    android:background="@android:color/white"
+    android:visibility="gone" >
+
+
+    <ImageView
+        android:id="@+id/zoomed_image_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="#000000"
+        android:padding="1dip" />
+
+    <ImageView
+        android:id="@+id/dialog_close"
+        android:background="@drawable/close"
+        android:layout_height="20dp"
+        android:layout_width="20dp"
+        android:layout_gravity ="top|right" />
+
+</org.mozilla.gecko.ZoomedView>
\ No newline at end of file
--- a/mobile/android/base/util/ThreadUtils.java
+++ b/mobile/android/base/util/ThreadUtils.java
@@ -91,16 +91,24 @@ public final class ThreadUtils {
     public static Handler getUiHandler() {
         return sUiHandler;
     }
 
     public static void postToUiThread(Runnable runnable) {
         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;
     }
 
     public static Handler getBackgroundHandler() {
         return GeckoBackgroundThread.getHandler();
     }
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4975,17 +4975,22 @@ var BrowserEventHandler = {
     if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
       return;
     }
 
     let target = aEvent.target;
     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 {
         Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
       } catch (e) {}
     }
     this._doTapHighlight(target);
   },
@@ -5092,16 +5097,17 @@ var BrowserEventHandler = {
             }
           }
         } catch(e) {
           Cu.reportError(e);
         }
 
         let data = JSON.parse(aData);
         let {x, y} = data;
+
         if (this._inCluster) {
           this._clusterClicked(x, y);
         } else {
           // The _highlightElement was chosen after fluffing the touch events
           // that led to this SingleTap, so by fluffing the mouse events, they
           // should find the same target since we fluff them again below.
           this._sendMouseEvent("mousemove", x, y);
           this._sendMouseEvent("mousedown", x, y);
@@ -5124,24 +5130,24 @@ var BrowserEventHandler = {
         break;
 
       default:
         dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
         break;
     }
   },
 
-  _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) {
     let metadata = BrowserApp.selectedTab.metadata;
     if (!metadata.allowDoubleTapZoom) {
       return;
     }
 
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -1699,16 +1699,104 @@ AndroidBridge::GetFrameNameJavaProfiling
 
     if (!jstrSampleName)
         return false;
 
     aResult = nsCString(jstrSampleName);
     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<uint8_t*> (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;
     float scale = 1.0;
 
     if (!buffer)
         return NS_ERROR_FAILURE;
 
--- a/widget/android/AndroidBridge.h
+++ b/widget/android/AndroidBridge.h
@@ -183,16 +183,17 @@ public:
     // us to use.  toolkit/xre/nsAndroidStartup.cpp calls
     // SetMainThread.
     bool SetMainThread(pthread_t thr);
 
     /* These are all implemented in Java */
     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();
     bool IsContentDocumentDisplayed();
 
     bool ProgressiveUpdateCallback(bool aHasPendingNewThebesContent, const LayerRect& aDisplayPort, float aDisplayResolution, bool aDrawingCritical,
                                    mozilla::ParentLayerPoint& aScrollOffset, mozilla::CSSToParentLayerScale& aZoom);
 
--- a/widget/android/AndroidJavaWrappers.cpp
+++ b/widget/android/AndroidJavaWrappers.cpp
@@ -533,16 +533,24 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jo
 
         case THUMBNAIL: {
             mMetaState = jenv->GetIntField(jobj, jMetaStateField);
             ReadPointArray(mPoints, jenv, jPoints, 1);
             mByteBuffer = new RefCountedJavaObject(jenv, jenv->GetObjectField(jobj, jByteBufferField));
             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;
         }
 
         case COMPOSITOR_CREATE: {
             mWidth = jenv->GetIntField(jobj, jWidthField);
             mHeight = jenv->GetIntField(jobj, jHeightField);
--- a/widget/android/AndroidJavaWrappers.h
+++ b/widget/android/AndroidJavaWrappers.h
@@ -741,16 +741,17 @@ public:
         PREFERENCES_GET = 40,
         PREFERENCES_REMOVE_OBSERVERS = 41,
         TELEMETRY_UI_SESSION_START = 42,
         TELEMETRY_UI_SESSION_STOP = 43,
         TELEMETRY_UI_EVENT = 44,
         GAMEPAD_ADDREMOVE = 45,
         GAMEPAD_DATA = 46,
         LONG_PRESS = 47,
+        ZOOMEDVIEW = 48,
         dummy_java_enum_list_end
     };
 
     enum {
         // Memory pressure levels. Keep these in sync with those in MemoryMonitor.java.
         MEMORY_PRESSURE_NONE = 0,
         MEMORY_PRESSURE_CLEANUP = 1,
         MEMORY_PRESSURE_LOW = 2,
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -975,16 +975,24 @@ constexpr char LayerView::name[];
 constexpr char LayerView::RegisterCompositorWrapper_t::name[];
 constexpr char LayerView::RegisterCompositorWrapper_t::signature[];
 
 mozilla::jni::Object::LocalRef LayerView::RegisterCompositorWrapper()
 {
     return mozilla::jni::Method<RegisterCompositorWrapper_t>::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<updateZoomedView_t>::Call(nullptr, nullptr, a0);
+}
+
 constexpr char NativePanZoomController::name[];
 
 constexpr char NativePanZoomController::RequestContentRepaintWrapper_t::name[];
 constexpr char NativePanZoomController::RequestContentRepaintWrapper_t::signature[];
 
 void NativePanZoomController::RequestContentRepaintWrapper(float a0, float a1, float a2, float a3, float a4) const
 {
     return mozilla::jni::Method<RequestContentRepaintWrapper_t>::Call(this, nullptr, a0, a1, a2, a3, a4);
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -1929,16 +1929,31 @@ public:
                 "()Lorg/mozilla/gecko/gfx/GLController;";
         static const bool isStatic = true;
         static const bool isMultithreaded = true;
         static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT;
     };
 
     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<NativePanZoomController> {
 
 public:
     typedef mozilla::jni::Ref<NativePanZoomController> Ref;
     typedef mozilla::jni::LocalRef<NativePanZoomController> LocalRef;
     typedef mozilla::jni::GlobalRef<NativePanZoomController> GlobalRef;
--- a/widget/android/nsAppShell.cpp
+++ b/widget/android/nsAppShell.cpp
@@ -388,16 +388,43 @@ nsAppShell::ProcessNextNativeEvent(bool 
         int32_t tabId = curEvent->MetaState();
         const nsTArray<nsIntPoint>& points = curEvent->Points();
         RefCountedJavaObject* buffer = curEvent->ByteBuffer();
         nsRefPtr<ThumbnailRunnable> sr = new ThumbnailRunnable(mBrowserApp, tabId, points, buffer);
         MessageLoop::current()->PostIdleTask(FROM_HERE, NewRunnableMethod(sr.get(), &ThumbnailRunnable::Run));
         break;
     }
 
+    case AndroidGeckoEvent::ZOOMEDVIEW: {
+        if (!mBrowserApp)
+            break;
+        int32_t tabId = curEvent->MetaState();
+        const nsTArray<nsIntPoint>& points = curEvent->Points();
+        float scaleFactor = (float) curEvent->X();
+        nsRefPtr<RefCountedJavaObject> javaBuffer = curEvent->ByteBuffer();
+        const auto& mBuffer = jni::Object::Ref::From(javaBuffer->GetObject());
+
+        nsCOMPtr<nsIDOMWindow> domWindow;
+        nsCOMPtr<nsIBrowserTab> 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)
             break;
 
         nsCOMPtr<nsIObserverService> obsServ =
             mozilla::services::GetObserverService();