private ImmutableViewportMetrics getValidViewportMetrics(
      ImmutableViewportMetrics viewportMetrics) {
    /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */
    float zoomFactor = viewportMetrics.zoomFactor;
    RectF pageRect = viewportMetrics.getPageRect();
    RectF viewport = viewportMetrics.getViewport();

    float focusX = viewport.width() / 2.0f;
    float focusY = viewport.height() / 2.0f;

    float minZoomFactor = 0.0f;
    float maxZoomFactor = MAX_ZOOM;

    ZoomConstraints constraints = mTarget.getZoomConstraints();

    if (constraints.getMinZoom() > 0) minZoomFactor = constraints.getMinZoom();
    if (constraints.getMaxZoom() > 0) maxZoomFactor = constraints.getMaxZoom();

    if (!constraints.getAllowZoom()) {
      // If allowZoom is false, clamp to the default zoom level.
      maxZoomFactor = minZoomFactor = constraints.getDefaultZoom();
    }

    // Ensure minZoomFactor keeps the page at least as big as the viewport.
    if (pageRect.width() > 0) {
      float scaleFactor = viewport.width() / pageRect.width();
      minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
      if (viewport.width() > pageRect.width()) focusX = 0.0f;
    }
    if (pageRect.height() > 0) {
      float scaleFactor = viewport.height() / pageRect.height();
      minZoomFactor = Math.max(minZoomFactor, zoomFactor * scaleFactor);
      if (viewport.height() > pageRect.height()) focusY = 0.0f;
    }

    maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor);

    if (zoomFactor < minZoomFactor) {
      // 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 = viewportMetrics.scaleTo(minZoomFactor, center);
    } else if (zoomFactor > maxZoomFactor) {
      PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f);
      viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center);
    }

    /* Now we pan to the right origin. */
    viewportMetrics = viewportMetrics.clamp();

    return viewportMetrics;
  }
  @Override
  public boolean onScale(SimpleScaleGestureDetector detector) {
    if (GeckoApp.mAppContext == null || GeckoApp.mAppContext.mDOMFullScreen) return false;

    if (mState != PanZoomState.PINCHING) 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(true), mY.getEdgeResistance(true));
    if (spanRatio > 1.0f) spanRatio = 1.0f + (spanRatio - 1.0f) * resistance;
    else spanRatio = 1.0f - (1.0f - spanRatio) * resistance;

    synchronized (mTarget.getLock()) {
      float newZoomFactor = getMetrics().zoomFactor * spanRatio;
      float minZoomFactor = 0.0f;
      float maxZoomFactor = MAX_ZOOM;

      ZoomConstraints constraints = mTarget.getZoomConstraints();

      if (constraints.getMinZoom() > 0) minZoomFactor = constraints.getMinZoom();
      if (constraints.getMaxZoom() > 0) maxZoomFactor = constraints.getMaxZoom();

      if (newZoomFactor < minZoomFactor) {
        // apply resistance when zooming past minZoomFactor,
        // such that it asymptotically reaches minZoomFactor / 2.0
        // but never exceeds that
        final float rate = 0.5f; // controls how quickly we approach the limit
        float excessZoom = minZoomFactor - newZoomFactor;
        excessZoom = 1.0f - (float) Math.exp(-excessZoom * rate);
        newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f);
      }

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

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

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

    GeckoEvent event =
        GeckoEvent.createNativeGestureEvent(
            GeckoEvent.ACTION_MAGNIFY, mLastZoomFocus, getMetrics().zoomFactor);
    GeckoAppShell.sendEventToGecko(event);

    return true;
  }