gt:         synchronized (getLock()) {
context: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
   * This Source Code Form is subject to the terms of the Mozilla Public
   * License, v. 2.0. If a copy of the MPL was not distributed with this
   * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  
  package org.mozilla.gecko.gfx;
  
  import org.mozilla.gecko.annotation.RobocopTarget;
  import org.mozilla.gecko.annotation.WrapForJNI;
  import org.mozilla.gecko.GeckoAppShell;
  import org.mozilla.gecko.GeckoEvent;
  import org.mozilla.gecko.gfx.LayerView.DrawListener;
  import org.mozilla.gecko.Tab;
  import org.mozilla.gecko.Tabs;
  import org.mozilla.gecko.ZoomConstraints;
  import org.mozilla.gecko.EventDispatcher;
  import org.mozilla.gecko.util.FloatUtils;
  import org.mozilla.gecko.AppConstants;
  
  import android.content.Context;
  import android.graphics.PointF;
  import android.graphics.RectF;
  import android.os.SystemClock;
  import android.util.DisplayMetrics;
  import android.util.Log;
  
  import java.util.ArrayList;
  import java.util.List;
  
  class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
  {
      private static final String LOGTAG = "GeckoLayerClient";
  
      private LayerRenderer mLayerRenderer;
      private boolean mLayerRendererInitialized;
  
      private final Context mContext;
      private IntSize mScreenSize;
      private IntSize mWindowSize;
      private DisplayPortMetrics mDisplayPort;
  
      private boolean mRecordDrawTimes;
      private final DrawTimingQueue mDrawTimingQueue;
  
      private VirtualLayer mRootLayer;
  
      /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread.
       * If any events being sent to Gecko that are relative to the Gecko viewport position,
       * they must (a) be relative to this viewport, and (b) be sent on the UI thread to
       * avoid races. As long as these two conditions are satisfied, and the events being
       * sent to Gecko are processed in FIFO order, the events will properly be relative
       * to the Gecko viewport position. Note that if Gecko updates its viewport independently,
       * we get notified synchronously and also update this on the UI thread.
       */
      private ImmutableViewportMetrics mGeckoViewport;
  
      /*
       * The viewport metrics being used to draw the current frame. This is only
       * accessed by the compositor thread, and so needs no synchronisation.
       */
      private ImmutableViewportMetrics mFrameMetrics;
  
      private final List<DrawListener> mDrawListeners;
  
      /* Used as temporaries by syncViewportInfo */
      private final ViewTransform mCurrentViewTransform;
      private final RectF mCurrentViewTransformMargins;
  
      /* Used as the return value of progressiveUpdateCallback */
      private final ProgressiveUpdateData mProgressiveUpdateData;
      private DisplayPortMetrics mProgressiveUpdateDisplayPort;
      private boolean mLastProgressiveUpdateWasLowPrecision;
      private boolean mProgressiveUpdateWasInDanger;
  
      private boolean mForceRedraw;
  
      /* The current viewport metrics.
       * This is volatile so that we can read and write to it from different threads.
       * We avoid synchronization to make getting the viewport metrics from
       * the compositor as cheap as possible. The viewport is immutable so
       * we don't need to worry about anyone mutating it while we're reading from it.
       * 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 mDynamicToolbarViewportChangeListener;
      private LayerView.OnMetricsChangedListener mZoomedViewViewportChangeListener;
  
      private ZoomConstraints mZoomConstraints;
  
      private boolean mGeckoIsReady;
  
      private final PanZoomController mPanZoomController;
      private final LayerMarginsAnimator mMarginsAnimator;
      private final LayerView mView;
  
      /* This flag is true from the time that browser.js detects a first-paint is about to start,
       * to the time that we receive the first-paint composite notification from the compositor.
       * Note that there is a small race condition with this; if there are two paints that both
       * have the first-paint flag set, and the second paint happens concurrently with the
       * composite for the first paint, then this flag may be set to true prematurely. Fixing this
       * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
       */
      private volatile boolean mContentDocumentIsDisplayed;
  
      public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
          // we can fill these in with dummy values because they are always written
          // to before being read
          mContext = context;
          mScreenSize = new IntSize(0, 0);
          mWindowSize = new IntSize(0, 0);
          mDisplayPort = new DisplayPortMetrics();
          mRecordDrawTimes = true;
          mDrawTimingQueue = new DrawTimingQueue();
          mCurrentViewTransform = new ViewTransform(0, 0, 1);
          mCurrentViewTransformMargins = new RectF();
          mProgressiveUpdateData = new ProgressiveUpdateData();
          mProgressiveUpdateDisplayPort = new DisplayPortMetrics();
  
          mForceRedraw = true;
          DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
          mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
                             .setViewportSize(view.getWidth(), view.getHeight());
          mZoomConstraints = new ZoomConstraints(false);
  
          Tab tab = Tabs.getInstance().getSelectedTab();
          if (tab != null) {
              mZoomConstraints = tab.getZoomConstraints();
              mViewportMetrics = mViewportMetrics.setIsRTL(tab.getIsRTL());
          }
  
          mFrameMetrics = mViewportMetrics;
  
          mDrawListeners = new ArrayList<DrawListener>();
          mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
          mMarginsAnimator = new LayerMarginsAnimator(this, view);
          mView = view;
          mView.setListener(this);
          mContentDocumentIsDisplayed = true;
      }
  
      public void setOverscrollHandler(final Overscroll listener) {
          mPanZoomController.setOverscrollHandler(listener);
      }
  
      /** Attaches to root layer so that Gecko appears. */
      public void notifyGeckoReady() {
          mGeckoIsReady = true;
  
          mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight()));
          mLayerRenderer = mView.getRenderer();
  
          sendResizeEventIfNecessary(true);
  
          DisplayPortCalculator.initPrefs();
  
          // Gecko being ready is one of the two conditions (along with having an available
          // surface) that cause us to create the compositor. So here, now that we know gecko
          // is ready, call updateCompositor() to see if we can actually do the creation.
          // This needs to run on the UI thread so that the surface validity can't change on
          // us while we're in the middle of creating the compositor.
          mView.post(new Runnable() {
              @Override
              public void run() {
                  mView.getGLController().updateCompositor();
              }
          });
      }
  
      public void destroy() {
          mPanZoomController.destroy();
          mMarginsAnimator.destroy();
          mDrawListeners.clear();
      }
  
      /**
       * Returns true if this client is fine with performing a redraw operation or false if it
       * would prefer that the action didn't take place.
       */
      private boolean getRedrawHint() {
          if (mForceRedraw) {
              mForceRedraw = false;
              return true;
          }
  
          if (!mPanZoomController.getRedrawHint()) {
              return false;
          }
  
          return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics,
                  mPanZoomController.getVelocityVector(), mDisplayPort);
      }
  
      Layer getRoot() {
          return mGeckoIsReady ? mRootLayer : null;
      }
  
      public LayerView getView() {
          return mView;
      }
  
      public FloatSize getViewportSize() {
          return mViewportMetrics.getSize();
      }
  
      /**
       * The view calls this function to indicate that the viewport changed size. It must hold the
       * monitor while calling it.
       *
       * TODO: Refactor this to use an interface. Expose that interface only to the view and not
       * to the layer client. That way, the layer client won't be tempted to call this, which might
       * result in an infinite loop.
       */
      void setViewportSize(int width, int height) {
          mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
  
          if (mGeckoIsReady) {
              // here we send gecko a resize message. The code in browser.js is responsible for
              // picking up on that resize event, modifying the viewport as necessary, and informing
              // us of the new viewport.
              sendResizeEventIfNecessary(true);
              // the following call also sends gecko a message, which will be processed after the resize
              // message above has updated the viewport. this message ensures that if we have just put
              // focus in a text field, we scroll the content so that the text field is in view.
              GeckoAppShell.viewSizeChanged();
          }
      }
  
      PanZoomController getPanZoomController() {
          return mPanZoomController;
      }
  
      LayerMarginsAnimator getLayerMarginsAnimator() {
          return mMarginsAnimator;
      }
  
      /* Informs Gecko that the screen size has changed. */
      private void sendResizeEventIfNecessary(boolean force) {
          DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
  
          IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
          IntSize newWindowSize = new IntSize(mView.getWidth(), mView.getHeight());
  
          boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
          boolean windowSizeChanged = !mWindowSize.equals(newWindowSize);
  
          if (!force && !screenSizeChanged && !windowSizeChanged) {
              return;
          }
  
          mScreenSize = newScreenSize;
          mWindowSize = newWindowSize;
  
          if (screenSizeChanged) {
              Log.d(LOGTAG, "Screen-size changed to " + mScreenSize);
          }
  
          if (windowSizeChanged) {
              Log.d(LOGTAG, "Window-size changed to " + mWindowSize);
          }
  
          GeckoEvent event = GeckoEvent.createSizeChangedEvent(mWindowSize.width, mWindowSize.height,
                                                               mScreenSize.width, mScreenSize.height);
          GeckoAppShell.sendEventToGecko(event);
          GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Window:Resize", ""));
      }
  
      /** Sets the current page rect. You must hold the monitor while calling this. */
      private void setPageRect(RectF rect, RectF cssRect) {
          // Since the "rect" is always just a multiple of "cssRect" we don't need to
          // check both; this function assumes that both "rect" and "cssRect" are relative
          // the zoom factor in mViewportMetrics.
          if (mViewportMetrics.getCssPageRect().equals(cssRect))
              return;
  
          mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect);
  
          // Page size is owned by the layer client, so no need to notify it of
          // this change.
  
          post(new Runnable() {
              @Override
              public void run() {
                  mPanZoomController.pageRectUpdated();
                  mView.requestRender();
              }
          });
      }
  
      /**
       * Derives content document fixed position margins/fixed layer margins from
       * the view margins in the given metrics object.
       */
      private void getFixedMargins(ImmutableViewportMetrics metrics, RectF fixedMargins) {
          fixedMargins.left = 0;
          fixedMargins.top = 0;
          fixedMargins.right = 0;
          fixedMargins.bottom = 0;
  
          // The maximum margins are determined by the scrollable area of the page.
          float maxMarginWidth = Math.max(0, metrics.getPageWidth() - metrics.getWidthWithoutMargins());
          float maxMarginHeight = Math.max(0, metrics.getPageHeight() - metrics.getHeightWithoutMargins());
  
          // If the margins can't fully hide, they're pinned on - in which case,
          // fixed margins should always be zero.
          if (maxMarginWidth < metrics.marginLeft + metrics.marginRight) {
            maxMarginWidth = 0;
          }
          if (maxMarginHeight < metrics.marginTop + metrics.marginBottom) {
            maxMarginHeight = 0;
          }
  
          PointF offset = metrics.getMarginOffset();
          RectF overscroll = metrics.getOverscroll();
          if (offset.x >= 0) {
              fixedMargins.right = Math.max(0, Math.min(offset.x - overscroll.right, maxMarginWidth));
          } else {
              fixedMargins.left = Math.max(0, Math.min(-offset.x - overscroll.left, maxMarginWidth));
          }
          if (offset.y >= 0) {
              fixedMargins.bottom = Math.max(0, Math.min(offset.y - overscroll.bottom, maxMarginHeight));
          } else {
              fixedMargins.top = Math.max(0, Math.min(-offset.y - overscroll.top, maxMarginHeight));
          }
  
          // Adjust for overscroll. If we're overscrolled on one side, add that
          // distance to the margins of the other side (limiting to the maximum
          // margin size calculated above).
          if (overscroll.left > 0) {
              fixedMargins.right = Math.min(maxMarginWidth - fixedMargins.left,
                                            fixedMargins.right + overscroll.left);
          } else if (overscroll.right > 0) {
              fixedMargins.left = Math.min(maxMarginWidth - fixedMargins.right,
                                           fixedMargins.left + overscroll.right);
          }
          if (overscroll.top > 0) {
              fixedMargins.bottom = Math.min(maxMarginHeight - fixedMargins.top,
                                             fixedMargins.bottom + overscroll.top);
          } else if (overscroll.bottom > 0) {
              fixedMargins.top = Math.min(maxMarginHeight - fixedMargins.bottom,
                                          fixedMargins.top + overscroll.bottom);
          }
      }
  
      private void adjustViewport(DisplayPortMetrics displayPort) {
          // TODO: APZ For fennec might need margins information to deal with
          // the dynamic toolbar.
          if (AppConstants.MOZ_ANDROID_APZ)
              return;
  
          ImmutableViewportMetrics metrics = getViewportMetrics();
          ImmutableViewportMetrics clampedMetrics = metrics.clamp();
  
          RectF margins = new RectF();
          getFixedMargins(metrics, margins);
          clampedMetrics = clampedMetrics.setMargins(
              margins.left, margins.top, margins.right, margins.bottom);
  
          if (displayPort == null) {
              displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector());
          }
  
          mDisplayPort = displayPort;
          mGeckoViewport = clampedMetrics;
  
          if (mRecordDrawTimes) {
              mDrawTimingQueue.add(displayPort);
          }
  
          GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort));
      }
  
      /** Aborts any pan/zoom animation that is currently in progress. */
      private void abortPanZoomAnimation() {
          if (mPanZoomController != null) {
              post(new Runnable() {
                  @Override
                  public void run() {
                      mPanZoomController.abortAnimation();
                  }
              });
          }
      }
  
      /**
       * The different types of Viewport messages handled. All viewport events
       * expect a display-port to be returned, but can handle one not being
       * returned.
       */
      private enum ViewportMessageType {
          UPDATE,       // The viewport has changed and should be entirely updated
          PAGE_SIZE     // The viewport's page-size has changed
      }
  
      /** Viewport message handler. */
      private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) {
  