LOAndroid3: (partially) render page with LOKitTileProvider

+ TileProvider & TileIterator interfaces
+ Clean-up obsolete mozilla stuff

Change-Id: Ief56f11bf7f8fd6da383ffc7be3461b765bf0157
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
index abcbe65..90825759d 100644
--- a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitThread.java
@@ -16,16 +16,10 @@ import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.libreoffice.kit.LibreOfficeKit;
import org.libreoffice.kit.Office;
import org.libreoffice.kit.Document;

public class LOKitThread extends Thread {
    private static final String LOGTAG = "GeckoThread";
    private static final int TILE_SIZE = 256;

    public Office mOffice;
    public Document mDocument;
    private TileProvider mTileProvider;

    public ConcurrentLinkedQueue<LOEvent> gEvents = new ConcurrentLinkedQueue<LOEvent>();
    private ViewportMetrics mViewportMetrics;
@@ -33,55 +27,40 @@ public class LOKitThread extends Thread {
    LOKitThread() {
    }

    private void openDocument() {
        // enable debugging messages as the first thing
        LibreOfficeKit.putenv("SAL_LOG=+WARN+INFO-INFO.legacy.osl-INFO.i18nlangtag");
        LibreOfficeKit.init(LibreOfficeMainActivity.mAppContext);

        mOffice = new Office(LibreOfficeKit.getLibreOfficeKitHandle());
        String input = "/assets/test1.odt";
        mDocument = mOffice.documentLoad(input);
    }

    private synchronized boolean draw() throws InterruptedException {
    private boolean draw() throws InterruptedException {
        final LibreOfficeMainActivity application = LibreOfficeMainActivity.mAppContext;

        openDocument();
        if (mTileProvider == null)
            mTileProvider = new LOKitTileProvider(application.getLayerController());

        long height = mDocument.getDocumentHeight();
        long width  = mDocument.getDocumentWidth();
        int pageWidth = mTileProvider.getPageWidth();
        int pageHeight = mTileProvider.getPageHeight();

        Log.e(LOGTAG, "Document Size: " + width + " " + height);
        String metadata = createJson(0, 0, pageWidth, pageHeight, pageWidth, pageHeight, 0, 0, 1.0);
        mViewportMetrics = new ViewportMetrics();

        int pageWidth = 1024;
        int pageHeight = 1024;
        boolean shouldContinue = application.getLayerClient().beginDrawing(pageWidth, pageHeight, TILE_SIZE, TILE_SIZE, metadata);

        String metadata = createJson(0, 0, 256, 256, pageWidth, pageHeight, 0, 0, 1.0);

        Rect bufferRect = application.getLayerClient().beginDrawing(256, 256, TILE_SIZE, TILE_SIZE, metadata);

        /*if (bufferRect == null) {
            Log.e(LOGTAG, "beginDrawing - false");
        if (!shouldContinue) {
            return false;
        }*/
        }

        Log.e(LOGTAG, "Filling tiles..");
        Log.i(LOGTAG, "Filling tiles..");

        ByteBuffer buffer = ByteBuffer.allocateDirect(TILE_SIZE * TILE_SIZE * 4);
        int x = 0;
        int y = 0;
        for (Bitmap bitmap : mTileProvider.getTileIterator()) {
            application.getLayerClient().addTile(bitmap, x, y);
            x += TILE_SIZE;
            if (x > pageWidth) {
                x = 0;
                y += TILE_SIZE;
            }
        }

        Log.e(LOGTAG, "PaintTile..");
        Log.i(LOGTAG, "End Draw");

        mDocument.paintTile(buffer, 256, 256, 1024, 1024, 4096, 4096);

        Log.e(LOGTAG, "EndPaintTile..");

        Bitmap bitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(buffer);

        application.getLayerClient().addTile(bitmap, 0, 0);

        Log.e(LOGTAG, "EndDrawing..");
        application.getLayerClient().endDrawing(0, 0, 256, 256);
        application.getLayerClient().endDrawing(0, 0, pageWidth, pageHeight);

        return true;
    }
@@ -143,22 +122,20 @@ public class LOKitThread extends Thread {
        try {
            boolean drawn = false;
            while (true) {

                if (!gEvents.isEmpty()) {
                    processEvent(gEvents.poll());
                } else {
                    if (!drawn) {
                        drawn = draw();
                    }
                    Thread.sleep(2000L);
                    Thread.sleep(100L);
                }
            }
        } catch (InterruptedException ex) {

        }
    }

    private synchronized void processEvent(LOEvent event) throws InterruptedException {
    private void processEvent(LOEvent event) throws InterruptedException {
        switch (event.mType) {
            case LOEvent.VIEWPORT:
                mViewportMetrics = event.getViewport();
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitTileProvider.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitTileProvider.java
new file mode 100644
index 0000000..4b6d8fa
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LOKitTileProvider.java
@@ -0,0 +1,103 @@
package org.libreoffice;

import android.graphics.Bitmap;
import android.util.Log;

import org.mozilla.gecko.gfx.LayerController;

import java.nio.ByteBuffer;
import java.util.Iterator;

import org.libreoffice.kit.LibreOfficeKit;
import org.libreoffice.kit.Office;
import org.libreoffice.kit.Document;

public class LOKitTileProvider implements TileProvider {
    private final LayerController mLayerController;

    public static int TILE_SIZE = 256;

    public final Office mOffice;
    public final Document mDocument;

    public LOKitTileProvider(LayerController layerController) {
        this.mLayerController = layerController;
        LibreOfficeKit.putenv("SAL_LOG=+WARN+INFO-INFO.legacy.osl-INFO.i18nlangtag");
        LibreOfficeKit.init(LibreOfficeMainActivity.mAppContext);

        mOffice = new Office(LibreOfficeKit.getLibreOfficeKitHandle());
        String input = "/assets/test1.odt";
        mDocument = mOffice.documentLoad(input);
    }

    @Override
    public int getPageWidth() {
        return (int) (mDocument.getDocumentWidth() / 1440.0 * LOKitShell.getDpi());
    }

    @Override
    public int getPageHeight() {
        return (int) (mDocument.getDocumentHeight() / 1440.0 * LOKitShell.getDpi());
    }

    public TileIterator getTileIterator() {
        return new LoKitTileIterator();
    }

    public class LoKitTileIterator implements TileIterator, Iterator<Bitmap> {
        private final double mTileWidth;
        private final double mTileHeight;

        private boolean mShouldContinue = true;

        private double mPositionWidth = 0;
        private double mPositionHeight = 0;

        private double mPageWidth;
        private double mPageHeight;

        public LoKitTileIterator() {
            mTileWidth  = (TILE_SIZE / (double) LOKitShell.getDpi()) * 1440.0;
            mTileHeight = (TILE_SIZE / (double) LOKitShell.getDpi()) * 1440.0;
            mPageWidth  = mDocument.getDocumentWidth();
            mPageHeight = mDocument.getDocumentHeight();
        }

        @Override
        public boolean hasNext() {
            return mShouldContinue;
        }

        @Override
        public Bitmap next() {
            ByteBuffer buffer = ByteBuffer.allocateDirect(TILE_SIZE * TILE_SIZE * 4);
            Bitmap bitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888);

            mDocument.paintTile(buffer, TILE_SIZE, TILE_SIZE, (int) mPositionWidth, (int) mPositionHeight, (int) mTileWidth, (int) mTileHeight);

            mPositionWidth += mTileWidth;

            if (mPositionWidth > mPageWidth) {
                mPositionHeight += mTileHeight;
                mPositionWidth = 0;
            }

            if (mPositionHeight > mPageHeight || mPositionHeight > 20000) {
                mShouldContinue = false;
            }

            bitmap.copyPixelsFromBuffer(buffer);
            return bitmap;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Iterator<Bitmap> iterator() {
            return this;
        }
    }
}
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java
index a3144e1..0f12208d 100644
--- a/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/LibreOfficeMainActivity.java
@@ -11,8 +11,6 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.os.Environment;
import java.io.File;

import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient;
import org.mozilla.gecko.gfx.LayerController;
@@ -63,9 +61,11 @@ public class LibreOfficeMainActivity extends Activity {
        mAppContext = this;

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - onCreate");

        setContentView(R.layout.activity_main);
        Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - onCreate");

        // setup gecko layout
        mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/MockTileProvider.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/MockTileProvider.java
new file mode 100644
index 0000000..04ebfb4
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/MockTileProvider.java
@@ -0,0 +1,64 @@
package org.libreoffice;

import android.graphics.Bitmap;

import org.apache.http.MethodNotSupportedException;
import org.mozilla.gecko.gfx.LayerController;

import java.util.Iterator;
import java.util.List;

public class MockTileProvider implements TileProvider {
    private final LayerController layerController;

    public MockTileProvider(LayerController layerController) {
        this.layerController = layerController;
    }

    @Override
    public int getPageWidth() {
        return 549;
    }

    @Override
    public int getPageHeight() {
        return 630;
    }

    public TileIterator getTileIterator() {
        return new MockTileIterator(layerController);
    }

    public class MockTileIterator implements TileIterator, Iterator<Bitmap> {
        private final LayerController layerController;

        private int tileNumber = 1;

        public MockTileIterator(LayerController layerController) {
            this.layerController = layerController;
        }

        @Override
        public boolean hasNext() {
            return tileNumber <= 9;
        }

        @Override
        public Bitmap next() {
            String imageName = "d" + tileNumber;
            tileNumber++;
            Bitmap bitmap = layerController.getDrawable(imageName);
            return bitmap;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Iterator<Bitmap> iterator() {
            return this;
        }
    }
}
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/TileIterator.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/TileIterator.java
new file mode 100644
index 0000000..68c39e5
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/TileIterator.java
@@ -0,0 +1,6 @@
package org.libreoffice;

import android.graphics.Bitmap;

public interface TileIterator extends Iterable<Bitmap> {
}
diff --git a/android/experimental/LOAndroid3/src/java/org/libreoffice/TileProvider.java b/android/experimental/LOAndroid3/src/java/org/libreoffice/TileProvider.java
new file mode 100644
index 0000000..a405fdf
--- /dev/null
+++ b/android/experimental/LOAndroid3/src/java/org/libreoffice/TileProvider.java
@@ -0,0 +1,14 @@
package org.libreoffice;


import android.graphics.Bitmap;

import java.util.List;

public interface TileProvider  {
    int getPageWidth();

    int getPageHeight();

    TileIterator getTileIterator();
}
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
index 134c406..c196cf8 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -59,15 +59,7 @@ import org.mozilla.gecko.util.FloatUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//import org.mozilla.gecko.GeckoApp;
//import org.mozilla.gecko.GeckoAppShell;
//import org.mozilla.gecko.GeckoEvent;

public abstract class GeckoLayerClient extends LayerClient implements GeckoEventListener {
    public static final int LAYER_CLIENT_TYPE_NONE = 0;
    public static final int LAYER_CLIENT_TYPE_SOFTWARE = 1;
    public static final int LAYER_CLIENT_TYPE_GL = 2;

public abstract class GeckoLayerClient implements GeckoEventListener {
    private static final String LOGTAG = "GeckoLayerClient";
    private static final long MIN_VIEWPORT_CHANGE_DELAY = 25L;
    private static Pattern sColorPattern;
@@ -88,53 +80,26 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
    // inside a transaction, so no synchronization is needed.
    private boolean mUpdateViewportOnEndDraw;
    private String mLastCheckerboardColor;
    /* Used by robocop for testing purposes */
    private DrawListener mDrawListener;

    protected LayerController mLayerController;

    public GeckoLayerClient(Context context) {
        mScreenSize = new IntSize(0, 0);
    }

    // Parses a color from an RGB triple of the form "rgb([0-9]+, [0-9]+, [0-9]+)". If the color
    // cannot be parsed, returns white.
    private static int parseColorFromGecko(String string) {
        if (sColorPattern == null) {
            sColorPattern = Pattern.compile("rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)");
        }

        Matcher matcher = sColorPattern.matcher(string);
        if (!matcher.matches()) {
            return Color.WHITE;
        }

        int r = Integer.parseInt(matcher.group(1));
        int g = Integer.parseInt(matcher.group(2));
        int b = Integer.parseInt(matcher.group(3));
        return Color.rgb(r, g, b);
    }

    protected abstract boolean setupLayer();

    protected abstract boolean shouldDrawProceed(int tileWidth, int tileHeight);

    protected abstract void updateLayerAfterDraw(Rect updatedRect);

    protected abstract IntSize getBufferSize();

    protected abstract IntSize getTileSize();

    protected abstract void tileLayerUpdated();

    public abstract Bitmap getBitmap();

    public abstract int getType();

    /**
     * Attaches the root layer to the layer controller so that Gecko appears.
     */
    @Override
    public void setLayerController(LayerController layerController) {
        super.setLayerController(layerController);
        mLayerController = layerController;

        layerController.setRoot(mTileLayer);
        if (mGeckoViewport != null) {
@@ -144,80 +109,25 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        sendResizeEventIfNecessary();
    }

    public Rect beginDrawing(int width, int height, int tileWidth, int tileHeight, String metadata) {

    public boolean beginDrawing(int width, int height, int tileWidth, int tileHeight, String metadata) {
        Log.e(LOGTAG, "### beginDrawing " + width + " " + height + " " + tileWidth + " " + tileHeight);

        if (setupLayer()) {
       if (setupLayer()) {
            Log.e(LOGTAG, "### Cancelling due to layer setup");
            return null;
            return false;
        }

        if (!shouldDrawProceed(tileWidth, tileHeight)) {
            Log.e(LOGTAG, "### Cancelling draw due to shouldDrawProceed()");
            return null;
        }

        LayerController controller = getLayerController();

        try {
            JSONObject viewportObject = new JSONObject(metadata);
            mNewGeckoViewport = new ViewportMetrics(viewportObject);

            Log.e(LOGTAG, "### beginDrawing new Gecko viewport " + mNewGeckoViewport);

            // Update the background color, if it's present.
            String backgroundColorString = viewportObject.optString("backgroundColor");
            if (backgroundColorString != null && !backgroundColorString.equals(mLastCheckerboardColor)) {
                mLastCheckerboardColor = backgroundColorString;
                controller.setCheckerboardColor(parseColorFromGecko(backgroundColorString));
            }
        } catch (JSONException e) {
            Log.e(LOGTAG, "Aborting draw, bad viewport description: " + metadata);
            return null;
            return false;
        }

        // Make sure we don't spend time painting areas we aren't interested in.
        // Only do this if the Gecko viewport isn't going to override our viewport.
        Rect bufferRect = new Rect(0, 0, width, height);

        if (!mUpdateViewportOnEndDraw) {
            // First, find out our ideal displayport. We do this by taking the
            // clamped viewport origin and taking away the optimum viewport offset.
            // This would be what we would send to Gecko if adjustViewport were
            // called now.
            ViewportMetrics currentMetrics = controller.getViewportMetrics();
            PointF currentBestOrigin = RectUtils.getOrigin(currentMetrics.getClampedViewport());
            PointF viewportOffset = currentMetrics.getOptimumViewportOffset(new IntSize(width, height));
            currentBestOrigin.offset(-viewportOffset.x, -viewportOffset.y);

            Rect currentRect = RectUtils.round(new RectF(currentBestOrigin.x, currentBestOrigin.y,
                    currentBestOrigin.x + width, currentBestOrigin.y + height));

            // Second, store Gecko's displayport.
            PointF currentOrigin = mNewGeckoViewport.getDisplayportOrigin();
            bufferRect = RectUtils.round(new RectF(currentOrigin.x, currentOrigin.y,
                    currentOrigin.x + width, currentOrigin.y + height));


            // Take the intersection of the two as the area we're interested in rendering.

            if (!bufferRect.intersect(currentRect)) {
                // If there's no intersection, we have no need to render anything,
                // but make sure to update the viewport size.
                beginTransaction(mTileLayer);
                try {
                    updateViewport(true);
                } finally {
                    endTransaction(mTileLayer);
                }
                return null;
            }
            bufferRect.offset(Math.round(-currentOrigin.x), Math.round(-currentOrigin.y));
        }

        beginTransaction(mTileLayer);
        return bufferRect;
        mTileLayer.beginTransaction();
        return true;
    }

    /*
@@ -225,23 +135,17 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
     * a little more JNI magic.
     */
    public void endDrawing(int x, int y, int width, int height) {
        synchronized (getLayerController()) {
        synchronized (mLayerController) {
            try {
                updateViewport(!mUpdateViewportOnEndDraw);
                mUpdateViewportOnEndDraw = false;

                Rect rect = new Rect(x, y, x + width, y + height);
                updateLayerAfterDraw(rect);
            } finally {
                endTransaction(mTileLayer);
                mTileLayer.endTransaction();
            }
        }
        Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - endDrawing");

        /* Used by robocop for testing purposes */
        if (mDrawListener != null) {
            mDrawListener.drawFinished(x, y, width, height);
        }
    }

    protected void updateViewport(boolean onlyUpdatePageSize) {
@@ -249,27 +153,25 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        // JS-side viewport dimensions override the java-side ones because
        // java is the One True Source of this information, and allowing JS
        // to override can lead to race conditions where this data gets clobbered.
        FloatSize viewportSize = getLayerController().getViewportSize();
        FloatSize viewportSize = mLayerController.getViewportSize();
        mGeckoViewport = mNewGeckoViewport;
        mGeckoViewport.setSize(viewportSize);

        LayerController controller = getLayerController();
        PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin();
        mTileLayer.setOrigin(PointUtils.round(displayportOrigin));
        mTileLayer.setResolution(mGeckoViewport.getZoomFactor());

        this.tileLayerUpdated();
        Log.e(LOGTAG, "### updateViewport onlyUpdatePageSize=" + onlyUpdatePageSize + " getTileViewport " + mGeckoViewport);

        if (onlyUpdatePageSize) {
            // Don't adjust page size when zooming unless zoom levels are
            // approximately equal.
            if (FloatUtils.fuzzyEquals(controller.getZoomFactor(), mGeckoViewport.getZoomFactor())) {
                controller.setPageSize(mGeckoViewport.getPageSize());
            if (FloatUtils.fuzzyEquals(mLayerController.getZoomFactor(), mGeckoViewport.getZoomFactor())) {
                mLayerController.setPageSize(mGeckoViewport.getPageSize());
            }
        } else {
            controller.setViewportMetrics(mGeckoViewport);
            controller.abortPanZoomAnimation();
            mLayerController.setViewportMetrics(mGeckoViewport);
            mLayerController.abortPanZoomAnimation();
        }
    }

@@ -284,14 +186,15 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        // size is zero (which indicates that the rendering surface hasn't been
        // allocated yet).
        boolean screenSizeChanged = (metrics.widthPixels != mScreenSize.width || metrics.heightPixels != mScreenSize.height);
        boolean viewportSizeValid = (getLayerController() != null && getLayerController().getViewportSize().isPositive());
        boolean viewportSizeValid = (mLayerController != null && mLayerController.getViewportSize().isPositive());

        if (!(force || (screenSizeChanged && viewportSizeValid))) {
            return;
        }

        mScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
        IntSize bufferSize = getBufferSize(), tileSize = getTileSize();
        IntSize bufferSize = getBufferSize();
        IntSize tileSize = getTileSize();

        Log.e(LOGTAG, "### Screen-size changed to " + mScreenSize);

@@ -301,13 +204,12 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        LOKitShell.sendEvent(event);
    }

    @Override
    public void render() {
        adjustViewportWithThrottling();
    }

    private void adjustViewportWithThrottling() {
        if (!getLayerController().getRedrawHint())
        if (!mLayerController.getRedrawHint())
            return;

        if (mPendingViewportAdjust)
@@ -315,7 +217,7 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent

        long timeDelta = System.currentTimeMillis() - mLastViewportChangeTime;
        if (timeDelta < MIN_VIEWPORT_CHANGE_DELAY) {
            getLayerController().getView().postDelayed(
            mLayerController.getView().postDelayed(
                    new Runnable() {
                        public void run() {
                            mPendingViewportAdjust = false;
@@ -330,13 +232,12 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        adjustViewport();
    }

    @Override
    public void viewportSizeChanged() {
        mViewportSizeChanged = true;
    }

    private void adjustViewport() {
        ViewportMetrics viewportMetrics = new ViewportMetrics(getLayerController().getViewportMetrics());
        ViewportMetrics viewportMetrics = new ViewportMetrics(mLayerController.getViewportMetrics());

        PointF viewportOffset = viewportMetrics.getOptimumViewportOffset(getBufferSize());
        viewportMetrics.setViewportOffset(viewportOffset);
@@ -366,7 +267,6 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
        }
    }

    @Override
    public void geometryChanged() {
        sendResizeEventIfNecessary();
        render();
@@ -381,18 +281,4 @@ public abstract class GeckoLayerClient extends LayerClient implements GeckoEvent
    private void sendResizeEventIfNecessary() {
        sendResizeEventIfNecessary(false);
    }

    /**
     * Used by robocop for testing purposes. Not for production use! This is called via reflection by robocop.
     */
    public void setDrawListener(DrawListener listener) {
        mDrawListener = listener;
    }

    /**
     * Used by robocop for testing purposes. Not for production use! This is used via reflection by robocop.
     */
    public interface DrawListener {
        public void drawFinished(int x, int y, int width, int height);
    }
}
\ No newline at end of file
\ No newline at end of file
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java
index de8076a..42bc0b6 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/GeckoSoftwareLayerClient.java
@@ -41,7 +41,7 @@ package org.mozilla.gecko.gfx;
import org.libreoffice.LOKitShell;
import org.mozilla.gecko.gfx.CairoImage;
import org.mozilla.gecko.gfx.IntSize;
import org.mozilla.gecko.gfx.LayerClient;
import org.mozilla.gecko.gfx.GeckoLayerClient;
import org.mozilla.gecko.gfx.LayerController;
import org.mozilla.gecko.gfx.LayerRenderer;
import org.mozilla.gecko.gfx.MultiTileLayer;
@@ -77,20 +77,11 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
        mFormat = CairoImage.FORMAT_ARGB32;
    }

    /*protected void finalize() throws Throwable {
        try {
            if (mBuffer != null)
                LOKitShell.freeDirectBuffer(mBuffer);
            mBuffer = null;
        } finally {
            super.finalize();
        }
    }*/

    public void setLayerController(LayerController layerController) {
        super.setLayerController(layerController);

        layerController.setRoot(mTileLayer);

        if (mGeckoViewport != null) {
            layerController.setViewportMetrics(mGeckoViewport);
        }
@@ -104,7 +95,7 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
        if(mTileLayer == null)
            mTileLayer = new MultiTileLayer(TILE_SIZE);

        getLayerController().setRoot(mTileLayer);
        mLayerController.setRoot(mTileLayer);

        // Force a resize event to be sent because the results of this
        // are different depending on what tile system we're using
@@ -114,22 +105,11 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
    }

    @Override
    protected boolean shouldDrawProceed(int tileWidth, int tileHeight) {
        // Make sure the tile-size matches. If it doesn't, we could crash trying
        // to access invalid memory.
        if (tileWidth != TILE_SIZE.width || tileHeight != TILE_SIZE.height) {
            Log.e(LOGTAG, "Aborting draw, incorrect tile size of " + tileWidth + "x" + tileHeight);
            return false;
        }
        return true;
    }
    public boolean beginDrawing(int width, int height, int tileWidth, int tileHeight, String metadata) {
        boolean shouldContinue = super.beginDrawing(width, height, tileWidth, tileHeight, metadata);

    @Override
    public Rect beginDrawing(int width, int height, int tileWidth, int tileHeight, String metadata) {
        Rect bufferRect = super.beginDrawing(width, height, tileWidth, tileHeight, metadata);

        if (bufferRect == null) {
            return bufferRect;
        if (!shouldContinue) {
            return shouldContinue;
        }

        // If the window size has changed, reallocate the buffer to match.
@@ -137,7 +117,7 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
            mBufferSize = new IntSize(width, height);
        }

        return bufferRect;
        return shouldContinue;
    }

    @Override
@@ -147,76 +127,6 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
        }
    }

    /*private void copyPixelsFromMultiTileLayer(Bitmap target) {
        Canvas c = new Canvas(target);
        ByteBuffer tileBuffer = mBuffer.slice();
        int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;

        for (int y = 0; y < mBufferSize.height; y += TILE_SIZE.height) {
            for (int x = 0; x < mBufferSize.width; x += TILE_SIZE.width) {
                // Calculate tile size
                IntSize tileSize = new IntSize(Math.min(mBufferSize.width - x, TILE_SIZE.width),
                        Math.min(mBufferSize.height - y, TILE_SIZE.height));

                // Create a Bitmap from this tile
                Bitmap tile = Bitmap.createBitmap(tileSize.width, tileSize.height,
                        CairoUtils.cairoFormatTobitmapConfig(mFormat));
                tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer());

                // Copy the tile to the master Bitmap and recycle it
                c.drawBitmap(tile, x, y, null);
                tile.recycle();

                // Progress the buffer to the next tile
                tileBuffer.position(tileSize.getArea() * bpp);
                tileBuffer = tileBuffer.slice();
            }
        }
    }*/

    @Override
    protected void tileLayerUpdated() {
        /* No-op. */
    }

    @Override
    public Bitmap getBitmap() {
        if (mTileLayer == null)
            return null;

        // Begin a tile transaction, otherwise the buffer can be destroyed while
        // we're reading from it.
        /*beginTransaction(mTileLayer);
        try {
            if (mBuffer == null || mBufferSize.width <= 0 || mBufferSize.height <= 0)
                return null;
            try {
                Bitmap b = null;

                if (mTileLayer instanceof MultiTileLayer) {
                    b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height,CairoUtils.cairoFormatTobitmapConfig(mFormat));
                    copyPixelsFromMultiTileLayer(b);
                } else {
                    Log.w(LOGTAG, "getBitmap() called on a layer (" + mTileLayer + ") we don't know how to get a bitmap from");
                }

                return b;
            } catch (OutOfMemoryError oom) {
                Log.w(LOGTAG, "Unable to create bitmap", oom);
                return null;
            }
        } finally {
            endTransaction(mTileLayer);
        }*/

        return null;
    }

    @Override
    public int getType() {
        return LAYER_CLIENT_TYPE_SOFTWARE;
    }

    @Override
    protected IntSize getBufferSize() {
        return new IntSize(
@@ -235,3 +145,4 @@ public class GeckoSoftwareLayerClient extends GeckoLayerClient {
        }
    }
}

diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java
deleted file mode 100644
index 4f46108..0000000
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerClient.java
+++ /dev/null
@@ -1,81 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Android code.
 *
 * The Initial Developer of the Original Code is Mozilla Foundation.
 * Portions created by the Initial Developer are Copyright (C) 2009-2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Patrick Walton <pcwalton@mozilla.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.gecko.gfx;

/**
 * A layer client provides tiles and manages other information used by the layer controller.
 */
public abstract class LayerClient {
    private LayerController mLayerController;

    public abstract void geometryChanged();

    public abstract void viewportSizeChanged();

    protected abstract void render();

    public LayerController getLayerController() {
        return mLayerController;
    }

    public void setLayerController(LayerController layerController) {
        mLayerController = layerController;
    }

    /**
     * A utility function for calling Layer.beginTransaction with the
     * appropriate LayerView.
     */
    public void beginTransaction(Layer aLayer) {
        if (mLayerController != null) {
            LayerView view = mLayerController.getView();
            if (view != null) {
                aLayer.beginTransaction(view);
                return;
            }
        }

        aLayer.beginTransaction();
    }

    // Included for symmetry.
    public void endTransaction(Layer aLayer) {
        aLayer.endTransaction();
    }
}

diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java
index 250dc84..e237052 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/gfx/LayerController.java
@@ -88,7 +88,7 @@ public class LayerController {
    private boolean mWaitForTouchListeners;
    private PanZoomController mPanZoomController;
    private OnTouchListener mOnTouchListener;       /* The touch listener. */
    private LayerClient mLayerClient;               /* The layer client. */
    private GeckoLayerClient mLayerClient;               /* The layer client. */
    /* The new color for the checkerboard. */
    private int mCheckerboardColor;
    private boolean mCheckerboardShouldShowChecks;
@@ -111,11 +111,11 @@ public class LayerController {
        mForceRedraw = true;
    }

    public LayerClient getLayerClient() {
    public GeckoLayerClient getLayerClient() {
        return mLayerClient;
    }

    public void setLayerClient(LayerClient layerClient) {
    public void setLayerClient(GeckoLayerClient layerClient) {
        mLayerClient = layerClient;
        layerClient.setLayerController(this);
    }
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java
index 8c0fce4..521e60a 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/Axis.java
@@ -88,7 +88,7 @@ abstract class Axis {
    private float mTouchPos;                /* Position of the most recent touch event on the current drag. */
    private float mLastTouchPos;            /* Position of the touch event before touchPos. */
    private float mVelocity;                /* Velocity in this direction; pixels per animation frame. */
    public boolean mScrollingDisabled;      /* Whether movement on this axis is locked. */
    private boolean mScrollingDisabled;     /* Whether movement on this axis is locked. */
    private boolean mDisableSnap;           /* Whether overscroll snapping is disabled. */
    private float mDisplacement;

@@ -147,7 +147,7 @@ abstract class Axis {
    }

    private Overscroll getOverscroll() {
        boolean minus = (getOrigin() < 0.0f);
        boolean minus = getOrigin() < 0.0f;
        boolean plus = (getViewportEnd() > getPageLength());
        if (minus && plus) {
            return Overscroll.BOTH;
@@ -164,10 +164,14 @@ abstract class Axis {
    // overscrolled on this axis, returns 0.
    private float getExcess() {
        switch (getOverscroll()) {
            case MINUS:     return -getOrigin();
            case PLUS:      return getViewportEnd() - getPageLength();
            case BOTH:      return getViewportEnd() - getPageLength() - getOrigin();
            default:        return 0.0f;
            case MINUS:
                return -getOrigin();
            case PLUS:
                return getViewportEnd() - getPageLength();
            case BOTH:
                return getViewportEnd() - getPageLength() - getOrigin();
            default:
                return 0.0f;
        }
    }

@@ -176,8 +180,7 @@ abstract class Axis {
     * possible and this axis has not been scroll locked while panning. Otherwise, returns false.
     */
    private boolean scrollable() {
        return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE &&
                !mScrollingDisabled;
        return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE && !mScrollingDisabled;
    }

    /*
diff --git a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
index c3caccc..066f4ce 100644
--- a/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
+++ b/android/experimental/LOAndroid3/src/java/org/mozilla/gecko/ui/PanZoomController.java
@@ -38,22 +38,23 @@

package org.mozilla.gecko.ui;

import org.json.JSONObject;
import org.json.JSONException;
import org.libreoffice.LOKitShell;
import org.libreoffice.LibreOfficeMainActivity;
import org.mozilla.gecko.gfx.FloatSize;
import org.mozilla.gecko.gfx.LayerController;
import org.mozilla.gecko.gfx.PointUtils;
import org.mozilla.gecko.gfx.ViewportMetrics;
import org.mozilla.gecko.util.FloatUtils;
import org.mozilla.gecko.GeckoEventListener;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.FloatMath;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;

import org.json.JSONObject;
import org.libreoffice.LOKitShell;
import org.libreoffice.LibreOfficeMainActivity;
import org.mozilla.gecko.GeckoEventListener;
import org.mozilla.gecko.gfx.FloatSize;
import org.mozilla.gecko.gfx.LayerController;
import org.mozilla.gecko.gfx.PointUtils;
import org.mozilla.gecko.gfx.ViewportMetrics;
import org.mozilla.gecko.util.FloatUtils;

import java.util.Timer;
import java.util.TimerTask;

@@ -65,29 +66,19 @@ import java.util.TimerTask;
 */
public class PanZoomController
        extends GestureDetector.SimpleOnGestureListener
        implements SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener
{
    private static final String LOGTAG = "GeckoPanZoomController";

    private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect";
    private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth";

    // Animation stops if the velocity is below this value when overscrolled or panning.
    private static final float STOPPED_THRESHOLD = 4.0f;

    // Animation stops is the velocity is below this threshold when flinging.
    private static final float FLING_STOPPED_THRESHOLD = 0.1f;

        implements SimpleScaleGestureDetector.SimpleScaleGestureListener, GeckoEventListener {
    // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
    // between the touch-down and touch-up of a click). In units of density-independent pixels.
    public static final float PAN_THRESHOLD = 1/16f * LOKitShell.getDpi();

    public static final float PAN_THRESHOLD = 1 / 16f * LOKitShell.getDpi();
    private static final String LOGTAG = "GeckoPanZoomController";
    // Animation stops if the velocity is below this value when overscrolled or panning.
    private static final float STOPPED_THRESHOLD = 4.0f;
    // Animation stops is the velocity is below this threshold when flinging.
    private static final float FLING_STOPPED_THRESHOLD = 0.1f;
    // Angle from axis within which we stay axis-locked
    private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees

    // The maximum amount we allow you to zoom into a page
    private static final float MAX_ZOOM = 4.0f;

    /* 16 precomputed frames of the _ease-out_ animation from the CSS Transitions specification. */
    private static final float[] EASE_OUT_ANIMATION_FRAMES = {
            0.00000f,   /* 0 */
@@ -107,27 +98,13 @@ public class PanZoomController
            0.97401f,   /* 14 */
            0.99309f,   /* 15 */
    };

    private enum PanZoomState {
        NOTHING,        /* no touch-start events received */
        FLING,          /* all touches removed, but we're still scrolling page */
        TOUCHING,       /* one touch-start event received */
        PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */
        PANNING,        /* panning without axis lock */
        PANNING_HOLD,   /* in panning, but not moving.
                         * similar to TOUCHING but after starting a pan */
        PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
        PINCHING,       /* nth touch-start, where n > 1. this mode allows pan and zoom */
        ANIMATED_ZOOM   /* animated zoom to a new rect */
    }

    private static String MESSAGE_ZOOM_RECT = "Browser:ZoomToRect";
    private static String MESSAGE_ZOOM_PAGE = "Browser:ZoomToPageWidth";
    private final LayerController mController;
    private final SubdocumentScrollHelper mSubscroller;
    private final Axis mX;
    private final Axis mY;

    private Thread mMainThread;

    /* The timer that handles flings or bounces. */
    private Timer mAnimationTimer;
    /* The runnable being scheduled by the animation timer. */
@@ -146,31 +123,19 @@ public class PanZoomController
        mY = new AxisY(mSubscroller);

        mMainThread = LibreOfficeMainActivity.mAppContext.getMainLooper().getThread();
        checkMainThread();

        mState = PanZoomState.NOTHING;

        //GeckoAppShell.registerGeckoEventListener(MESSAGE_ZOOM_RECT, this);
        //GeckoAppShell.registerGeckoEventListener(MESSAGE_ZOOM_PAGE, this);
    }

    // for debugging bug 713011; it can be taken out once that is resolved.
    private void checkMainThread() {
        if (mMainThread != Thread.currentThread()) {
            // log with full stack trace
            Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception());
        }
    }

    public void handleMessage(String event, JSONObject message) {
        Log.i(LOGTAG, "Got message: " + event);
        try {
            if (MESSAGE_ZOOM_RECT.equals(event)) {
                float x = (float)message.getDouble("x");
                float y = (float)message.getDouble("y");
                float x = (float) message.getDouble("x");
                float y = (float) message.getDouble("y");
                final RectF zoomRect = new RectF(x, y,
                        x + (float)message.getDouble("w"),
                        y + (float)message.getDouble("h"));
                        x + (float) message.getDouble("w"),
                        y + (float) message.getDouble("h"));
                mController.post(new Runnable() {
                    public void run() {
                        animatedZoomTo(zoomRect);
@@ -185,9 +150,9 @@ public class PanZoomController
                float newHeight = viewableRect.height() * pageSize.width / viewableRect.width();
                float dh = viewableRect.height() - newHeight; // increase in the height
                final RectF r = new RectF(0.0f,
                        y + dh/2,
                        y + dh / 2,
                        pageSize.width,
                        y + dh/2 + newHeight);
                        y + dh / 2 + newHeight);
                mController.post(new Runnable() {
                    public void run() {
                        animatedZoomTo(r);
@@ -201,17 +166,23 @@ public class PanZoomController

    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:   return onTouchStart(event);
            case MotionEvent.ACTION_MOVE:   return onTouchMove(event);
            case MotionEvent.ACTION_UP:     return onTouchEnd(event);
            case MotionEvent.ACTION_CANCEL: return onTouchCancel(event);
            default:                        return false;
            case MotionEvent.ACTION_DOWN:
                return onTouchStart(event);
            case MotionEvent.ACTION_MOVE:
                return onTouchMove(event);
            case MotionEvent.ACTION_UP:
                return onTouchEnd(event);
            case MotionEvent.ACTION_CANCEL:
                return onTouchCancel(event);
            default:
                return false;
        }
    }

    /** This function must be called from the UI thread. */
    /**
     * This function must be called from the UI thread.
     */
    public void abortAnimation() {
        checkMainThread();
        // this happens when gecko changes the viewport on us or if the device is rotated.
        // if that's the case, abort any animation in progress and re-zoom so that the page
        // snaps to edges. for other cases (where the user's finger(s) are down) don't do
@@ -235,11 +206,13 @@ public class PanZoomController
        }
    }

    /** This must be called on the UI thread. */
    /**
     * This must be called on the UI thread.
     */
    public void pageSizeUpdated() {
        if (mState == PanZoomState.NOTHING) {
            ViewportMetrics validated = getValidViewportMetrics();
            if (! mController.getViewportMetrics().fuzzyEquals(validated)) {
            if (!mController.getViewportMetrics().fuzzyEquals(validated)) {
                // page size changed such that we are now in overscroll. snap to the
                // the nearest valid viewport
                mController.setViewportMetrics(validated);
@@ -248,10 +221,6 @@ public class PanZoomController
        }
    }

    /*
     * Panning/scrolling
     */

    private boolean onTouchStart(MotionEvent event) {
        Log.d(LOGTAG, "onTouchStart in state " + mState);
        // user is taking control of movement, so stop
@@ -279,6 +248,9 @@ public class PanZoomController
        return false;
    }

    /*
     * Panning/scrolling
     */
    private boolean onTouchMove(MotionEvent event) {
        Log.d(LOGTAG, "onTouchMove in state " + mState);

@@ -295,8 +267,6 @@ public class PanZoomController
                }
                cancelTouch();
                startPanning(event.getX(0), event.getY(0), event.getEventTime());
                //GeckoApp.mAppContext.hidePlugins(false /* don't hide layers */);
                //GeckoApp.mAutoCompletePopup.hide();
                track(event);
                return true;

@@ -404,7 +374,7 @@ public class PanZoomController
    }

    private void track(float x, float y, long time) {
        float timeDelta = (float)(time - mLastEventTime);
        float timeDelta = (float) (time - mLastEventTime);
        if (FloatUtils.fuzzyEquals(timeDelta, 0)) {
            // probably a duplicate event, ignore it. using a zero timeDelta will mess
            // up our velocity
@@ -490,8 +460,10 @@ public class PanZoomController
        mAnimationRunnable = runnable;
        mAnimationTimer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() { mController.post(runnable); }
        }, 0, 1000L/60L);
            public void run() {
                mController.post(runnable);
            }
        }, 0, 1000L / 60L);
    }

    /* Stops the fling or bounce animation. */
@@ -504,14 +476,12 @@ public class PanZoomController
            mAnimationRunnable.terminate();
            mAnimationRunnable = null;
        }

        //GeckoApp.mAppContext.showPlugins();
    }

    private float getVelocity() {
        float xvel = mX.getRealVelocity();
        float yvel = mY.getRealVelocity();
        return FloatMath.sqrt(xvel * xvel + yvel * yvel);
        float xVelocity = mX.getRealVelocity();
        float yVelocity = mY.getRealVelocity();
        return FloatMath.sqrt(xVelocity * xVelocity + yVelocity * yVelocity);
    }

    private boolean stopped() {
@@ -526,13 +496,229 @@ public class PanZoomController
        mX.displace();
        mY.displace();
        PointF displacement = getDisplacement();
        if (! mSubscroller.scrollBy(displacement)) {
        if (!mSubscroller.scrollBy(displacement)) {
            synchronized (mController) {
                mController.scrollBy(displacement);
            }
        }
    }

    private void finishAnimation() {
        Log.d(LOGTAG, "Finishing animation at " + mController.getViewportMetrics());
        stopAnimationTimer();

        // Force a viewport synchronisation
        mController.setForceRedraw();
        mController.notifyLayerClientOfGeometryChange();
    }

    /* Returns the nearest viewport metrics with no overscroll visible. */
    private ViewportMetrics getValidViewportMetrics() {
        return getValidViewportMetrics(new ViewportMetrics(mController.getViewportMetrics()));
    }

    private ViewportMetrics getValidViewportMetrics(ViewportMetrics viewportMetrics) {
        Log.d(LOGTAG, "generating valid viewport using " + viewportMetrics);

        /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
        float zoomFactor = viewportMetrics.getZoomFactor();
        FloatSize pageSize = viewportMetrics.getPageSize();
        RectF viewport = viewportMetrics.getViewport();

        float focusX = viewport.width() / 2.0f;
        float focusY = viewport.height() / 2.0f;
        float minZoomFactor = 0.0f;
        if (viewport.width() > pageSize.width && pageSize.width > 0) {
            float scaleFactor = viewport.width() / pageSize.width;
            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
            focusX = 0.0f;
        }
        if (viewport.height() > pageSize.height && pageSize.height > 0) {
            float scaleFactor = viewport.height() / pageSize.height;
            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
            focusY = 0.0f;
        }

        if (!FloatUtils.fuzzyEquals(minZoomFactor, 0.0f)) {
            // if one (or both) of the page dimensions is smaller than the viewport,
            // zoom using the top/left as the focus on that axis. this prevents the
            // scenario where, if both dimensions are smaller than the viewport, but
            // by different scale factors, we end up scrolled to the end on one axis
            // after applying the scale
            PointF center = new PointF(focusX, focusY);
            viewportMetrics.scaleTo(minZoomFactor, center);
        } else if (zoomFactor > MAX_ZOOM) {
            PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
            viewportMetrics.scaleTo(MAX_ZOOM, center);
        }

        /* Now we pan to the right origin. */
        viewportMetrics.setViewport(viewportMetrics.getClampedViewport());
        Log.d(LOGTAG, "generated valid viewport as " + viewportMetrics);

        return viewportMetrics;
    }

    /*
     * Zooming
     */
    @Override
    public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScaleBegin in " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return false;

        mState = PanZoomState.PINCHING;
        mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());

        cancelTouch();

        return true;
    }

    @Override
    public boolean onScale(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScale in state " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return false;

        float prevSpan = detector.getPreviousSpan();
        if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
            // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
            return true;
        }

        float spanRatio = detector.getCurrentSpan() / prevSpan;

        /*
         * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
         * factor toward 1.0.
         */
        float resistance = Math.min(mX.getEdgeResistance(), mY.getEdgeResistance());
        if (spanRatio > 1.0f) {
            spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
        } else {
            spanRatio = 1.0f - (1.0f - spanRatio) * resistance;
        }

        synchronized (mController) {
            float newZoomFactor = mController.getZoomFactor() * spanRatio;
            if (newZoomFactor >= MAX_ZOOM) {
                // apply resistance when zooming past MAX_ZOOM,
                // such that it asymptotically reaches MAX_ZOOM + 1.0
                // but never exceeds that
                float excessZoom = newZoomFactor - MAX_ZOOM;
                excessZoom = 1.0f - (float) Math.exp(-excessZoom);
                newZoomFactor = MAX_ZOOM + excessZoom;
            }

            mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(),
                    mLastZoomFocus.y - detector.getFocusY()));
            PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
            mController.scaleWithFocus(newZoomFactor, focus);
        }

        mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());

        return true;
    }

    @Override
    public void onScaleEnd(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScaleEnd in " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return;

        // switch back to the touching state
        startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());

        // Force a viewport synchronisation
        //GeckoApp.mAppContext.showPlugins();
        mController.setForceRedraw();
        mController.notifyLayerClientOfGeometryChange();
    }

    public boolean getRedrawHint() {
        return (mState != PanZoomState.PINCHING && mState != PanZoomState.ANIMATED_ZOOM);
    }

    @Override
    public void onLongPress(MotionEvent motionEvent) {
    }

    @Override
    public boolean onDown(MotionEvent motionEvent) {
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
        return true;
    }

    @Override
    public boolean onDoubleTap(MotionEvent motionEvent) {
        return true;
    }

    public void cancelTouch() {
    }

    private boolean animatedZoomTo(RectF zoomToRect) {
        mState = PanZoomState.ANIMATED_ZOOM;
        final float startZoom = mController.getZoomFactor();

        RectF viewport = mController.getViewport();
        // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
        // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
        // while enlarging make sure we enlarge equally on both sides to keep the target rect
        // centered.
        float targetRatio = viewport.width() / viewport.height();
        float rectRatio = zoomToRect.width() / zoomToRect.height();
        if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
            // all good, do nothing
        } else if (targetRatio < rectRatio) {
            // need to increase zoomToRect height
            float newHeight = zoomToRect.width() / targetRatio;
            zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
            zoomToRect.bottom = zoomToRect.top + newHeight;
        } else { // targetRatio > rectRatio) {
            // need to increase zoomToRect width
            float newWidth = targetRatio * zoomToRect.height();
            zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
            zoomToRect.right = zoomToRect.left + newWidth;
        }

        float finalZoom = viewport.width() * startZoom / zoomToRect.width();

        ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics());
        finalMetrics.setOrigin(new PointF(zoomToRect.left, zoomToRect.top));
        finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));

        // 2. now run getValidViewportMetrics on it, so that the target viewport is
        // clamped down to prevent overscroll, over-zoom, and other bad conditions.
        finalMetrics = getValidViewportMetrics(finalMetrics);

        bounce(finalMetrics);
        return true;
    }

    private enum PanZoomState {
        NOTHING,        /* no touch-start events received */
        FLING,          /* all touches removed, but we're still scrolling page */
        TOUCHING,       /* one touch-start event received */
        PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */
        PANNING,        /* panning without axis lock */
        PANNING_HOLD,   /* in panning, but not moving.
                         * similar to TOUCHING but after starting a pan */
        PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */
        PINCHING,       /* nth touch-start, where n > 1. this mode allows pan and zoom */
        ANIMATED_ZOOM   /* animated zoom to a new rect */
    }

    private abstract class AnimationRunnable implements Runnable {
        private boolean mAnimationTerminated;

@@ -667,255 +853,45 @@ public class PanZoomController
        }
    }

    private void finishAnimation() {
        checkMainThread();

        Log.d(LOGTAG, "Finishing animation at " + mController.getViewportMetrics());
        stopAnimationTimer();

        // Force a viewport synchronisation
        //GeckoApp.mAppContext.showPlugins();
        mController.setForceRedraw();
        mController.notifyLayerClientOfGeometryChange();
    }

    /* Returns the nearest viewport metrics with no overscroll visible. */
    private ViewportMetrics getValidViewportMetrics() {
        return getValidViewportMetrics(new ViewportMetrics(mController.getViewportMetrics()));
    }

    private ViewportMetrics getValidViewportMetrics(ViewportMetrics viewportMetrics) {
        Log.d(LOGTAG, "generating valid viewport using " + viewportMetrics);

        /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
        float zoomFactor = viewportMetrics.getZoomFactor();
        FloatSize pageSize = viewportMetrics.getPageSize();
        RectF viewport = viewportMetrics.getViewport();

        float focusX = viewport.width() / 2.0f;
        float focusY = viewport.height() / 2.0f;
        float minZoomFactor = 0.0f;
        if (viewport.width() > pageSize.width && pageSize.width > 0) {
            float scaleFactor = viewport.width() / pageSize.width;
            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
            focusX = 0.0f;
        }
        if (viewport.height() > pageSize.height && pageSize.height > 0) {
            float scaleFactor = viewport.height() / pageSize.height;
            minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
            focusY = 0.0f;
        }

        if (!FloatUtils.fuzzyEquals(minZoomFactor, 0.0f)) {
            // if one (or both) of the page dimensions is smaller than the viewport,
            // zoom using the top/left as the focus on that axis. this prevents the
            // scenario where, if both dimensions are smaller than the viewport, but
            // by different scale factors, we end up scrolled to the end on one axis
            // after applying the scale
            PointF center = new PointF(focusX, focusY);
            viewportMetrics.scaleTo(minZoomFactor, center);
        } else if (zoomFactor > MAX_ZOOM) {
            PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
            viewportMetrics.scaleTo(MAX_ZOOM, center);
        }

        /* Now we pan to the right origin. */
        viewportMetrics.setViewport(viewportMetrics.getClampedViewport());
        Log.d(LOGTAG, "generated valid viewport as " + viewportMetrics);

        return viewportMetrics;
    }

    private class AxisX extends Axis {
        AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); }
        AxisX(SubdocumentScrollHelper subscroller) {
            super(subscroller);
        }

        @Override
        public float getOrigin() { return mController.getOrigin().x; }
        public float getOrigin() {
            return mController.getOrigin().x;
        }

        @Override
        protected float getViewportLength() { return mController.getViewportSize().width; }
        protected float getViewportLength() {
            return mController.getViewportSize().width;
        }

        @Override
        protected float getPageLength() { return mController.getPageSize().width; }
        protected float getPageLength() {
            return mController.getPageSize().width;
        }
    }

    private class AxisY extends Axis {
        AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); }
        AxisY(SubdocumentScrollHelper subscroller) {
            super(subscroller);
        }

        @Override
        public float getOrigin() { return mController.getOrigin().y; }
        public float getOrigin() {
            return mController.getOrigin().y;
        }

        @Override
        protected float getViewportLength() { return mController.getViewportSize().height; }
        protected float getViewportLength() {
            return mController.getViewportSize().height;
        }

        @Override
        protected float getPageLength() { return mController.getPageSize().height; }
    }

    /*
     * Zooming
     */
    @Override
    public boolean onScaleBegin(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScaleBegin in " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return false;

        mState = PanZoomState.PINCHING;
        mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY());
        //GeckoApp.mAppContext.hidePlugins(false /* don't hide layers, only views */);
        //GeckoApp.mAutoCompletePopup.hide();
        cancelTouch();

        return true;
    }

    @Override
    public boolean onScale(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScale in state " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return false;

        float prevSpan = detector.getPreviousSpan();
        if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) {
            // let's eat this one to avoid setting the new zoom to infinity (bug 711453)
            return true;
        protected float getPageLength() {
            return mController.getPageSize().height;
        }

        float spanRatio = detector.getCurrentSpan() / prevSpan;

        /*
         * Apply edge resistance if we're zoomed out smaller than the page size by scaling the zoom
         * factor toward 1.0.
         */
        float resistance = Math.min(mX.getEdgeResistance(), mY.getEdgeResistance());
        if (spanRatio > 1.0f)
            spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
        else
            spanRatio = 1.0f - (1.0f - spanRatio) * resistance;

        synchronized (mController) {
            float newZoomFactor = mController.getZoomFactor() * spanRatio;
            if (newZoomFactor >= MAX_ZOOM) {
                // apply resistance when zooming past MAX_ZOOM,
                // such that it asymptotically reaches MAX_ZOOM + 1.0
                // but never exceeds that
                float excessZoom = newZoomFactor - MAX_ZOOM;
                excessZoom = 1.0f - (float)Math.exp(-excessZoom);
                newZoomFactor = MAX_ZOOM + excessZoom;
            }

            mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(),
                    mLastZoomFocus.y - detector.getFocusY()));
            PointF focus = new PointF(detector.getFocusX(), detector.getFocusY());
            mController.scaleWithFocus(newZoomFactor, focus);
        }

        mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY());

        return true;
    }

    @Override
    public void onScaleEnd(SimpleScaleGestureDetector detector) {
        Log.d(LOGTAG, "onScaleEnd in " + mState);

        if (mState == PanZoomState.ANIMATED_ZOOM)
            return;

        // switch back to the touching state
        startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime());

        // Force a viewport synchronisation
        //GeckoApp.mAppContext.showPlugins();
        mController.setForceRedraw();
        mController.notifyLayerClientOfGeometryChange();
    }

    public boolean getRedrawHint() {
        return (mState != PanZoomState.PINCHING && mState != PanZoomState.ANIMATED_ZOOM);
    }

    private void sendPointToGecko(String event, MotionEvent motionEvent) {
        String json;
        try {
            PointF point = new PointF(motionEvent.getX(), motionEvent.getY());
            point = mController.convertViewPointToLayerPoint(point);
            if (point == null) {
                return;
            }
            json = PointUtils.toJSON(point).toString();
        } catch (Exception e) {
            Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e);
            return;
        }

        //GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json));
    }

    @Override
    public void onLongPress(MotionEvent motionEvent) {
        sendPointToGecko("Gesture:LongPress", motionEvent);
    }

    @Override
    public boolean onDown(MotionEvent motionEvent) {
        sendPointToGecko("Gesture:ShowPress", motionEvent);
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
        //GeckoApp.mAutoCompletePopup.hide();
        sendPointToGecko("Gesture:SingleTap", motionEvent);
        return true;
    }

    @Override
    public boolean onDoubleTap(MotionEvent motionEvent) {
        sendPointToGecko("Gesture:DoubleTap", motionEvent);
        return true;
    }

    public void cancelTouch() {
        //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
        //GeckoAppShell.sendEventToGecko(e);
    }

    private boolean animatedZoomTo(RectF zoomToRect) {
        //GeckoApp.mAutoCompletePopup.hide();

        mState = PanZoomState.ANIMATED_ZOOM;
        final float startZoom = mController.getZoomFactor();

        RectF viewport = mController.getViewport();
        // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport,
        // enlarging as necessary (if it gets too big, it will get shrunk in the next step).
        // while enlarging make sure we enlarge equally on both sides to keep the target rect
        // centered.
        float targetRatio = viewport.width() / viewport.height();
        float rectRatio = zoomToRect.width() / zoomToRect.height();
        if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) {
            // all good, do nothing
        } else if (targetRatio < rectRatio) {
            // need to increase zoomToRect height
            float newHeight = zoomToRect.width() / targetRatio;
            zoomToRect.top -= (newHeight - zoomToRect.height()) / 2;
            zoomToRect.bottom = zoomToRect.top + newHeight;
        } else { // targetRatio > rectRatio) {
            // need to increase zoomToRect width
            float newWidth = targetRatio * zoomToRect.height();
            zoomToRect.left -= (newWidth - zoomToRect.width()) / 2;
            zoomToRect.right = zoomToRect.left + newWidth;
        }

        float finalZoom = viewport.width() * startZoom / zoomToRect.width();

        ViewportMetrics finalMetrics = new ViewportMetrics(mController.getViewportMetrics());
        finalMetrics.setOrigin(new PointF(zoomToRect.left, zoomToRect.top));
        finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f));

        // 2. now run getValidViewportMetrics on it, so that the target viewport is
        // clamped down to prevent overscroll, over-zoom, and other bad conditions.
        finalMetrics = getValidViewportMetrics(finalMetrics);

        bounce(finalMetrics);
        return true;
    }
}