[Magnifier - 3] Reduce number of calls to PixelCopy

* Magnifier#show() takes snapshots of the content and displays
  them in the Magnifier bitmap
* calling show(...) consecutive times with the same arguments is
  a no-op if already showing (to cater with the miriad of motion
  events produced by touch which end up calling show(...))
* introduced Magnifier#invalidate(...) which, if currently
  showing, forces updating the content using the last configuration
* clamped the start horizontal value of the Rect which delimits the
  content to show in order to avoid distorting the rendering of the
  magnifier content
* fixed invalidating the magnifier (invalidate() does not
  automatically call invalidate(RectF) !)

Bug: 63531115
Bug: 67296158
Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: manual test that shows the magnifier working
Change-Id: I8e53dfb6582d541922fe05b60311658fb07ca880
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 91f6799..d4be7e5 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -476,6 +476,17 @@
         stopTextActionModeWithPreservingSelection();
     }
 
+    void invalidateMagnifier() {
+        final DisplayMetrics dm = mTextView.getResources().getDisplayMetrics();
+        invalidateMagnifier(0, 0, dm.widthPixels, dm.heightPixels);
+    }
+
+    void invalidateMagnifier(final float l, final float t, final float r, final float b) {
+        if (mMagnifier != null) {
+            mTextView.post(() -> mMagnifier.invalidate(new RectF(l, t, r, b)));
+        }
+    }
+
     private void discardTextDisplayLists() {
         if (mTextRenderNodes != null) {
             for (int i = 0; i < mTextRenderNodes.length; i++) {
@@ -4545,10 +4556,8 @@
                     + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f;
             final int[] coordinatesOnScreen = new int[2];
             mTextView.getLocationOnScreen(coordinatesOnScreen);
-            final float centerXOnScreen = xPosInView + mTextView.getTotalPaddingLeft()
-                    - mTextView.getScrollX() + coordinatesOnScreen[0];
-            final float centerYOnScreen = yPosInView + mTextView.getTotalPaddingTop()
-                    - mTextView.getScrollY() + coordinatesOnScreen[1];
+            final float centerXOnScreen = mTextView.convertViewToScreenCoord(xPosInView, true);
+            final float centerYOnScreen = mTextView.convertViewToScreenCoord(yPosInView, false);
 
             suspendBlink();
             mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d9bc51f..ce80552 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9219,6 +9219,36 @@
         }
     }
 
+    @Override
+    public void invalidate() {
+        super.invalidate();
+
+        if (mEditor != null) {
+            mEditor.invalidateMagnifier();
+        }
+    }
+
+    @Override
+    public void invalidate(int l, int t, int r, int b) {
+        super.invalidate(l, t, r, b);
+
+        if (mEditor != null) {
+            mEditor.invalidateMagnifier(
+                    convertViewToScreenCoord(l, true /* isHorizontal */),
+                    convertViewToScreenCoord(t, false /* isHorizontal */),
+                    convertViewToScreenCoord(r, true /* isHorizontal */),
+                    convertViewToScreenCoord(b, false /* isHorizontal */));
+        }
+    }
+
+    float convertViewToScreenCoord(float viewCoord, boolean isHorizontal) {
+        final int[] coordinatesOnScreen = new int[2];
+        getLocationOnScreen(coordinatesOnScreen);
+        return isHorizontal
+                ? viewCoord + getTotalPaddingLeft() - getScrollX() + coordinatesOnScreen[0]
+                : viewCoord + getTotalPaddingTop() - getScrollY() + coordinatesOnScreen[1];
+    }
+
     /**
      * @return whether or not the cursor is visible (assuming this TextView is editable)
      *
diff --git a/core/java/com/android/internal/widget/Magnifier.java b/core/java/com/android/internal/widget/Magnifier.java
index 284f2b2..9bc0778 100644
--- a/core/java/com/android/internal/widget/Magnifier.java
+++ b/core/java/com/android/internal/widget/Magnifier.java
@@ -22,7 +22,9 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Handler;
 import android.util.Log;
 import android.view.Gravity;
@@ -41,8 +43,8 @@
  */
 public final class Magnifier {
     private static final String LOG_TAG = "magnifier";
-    private static final int MINIMUM_MAGNIFIER_SCALE = 1;
-    private static final int MAXIMUM_MAGNIFIER_SCALE = 4;
+    // Use this to specify that a previous configuration value does not exist.
+    private static final int INEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
     // The view for which this magnifier is attached.
     private final View mView;
     // The window containing the magnifier.
@@ -61,6 +63,15 @@
     // the copy is finished.
     private final Handler mPixelCopyHandler = Handler.getMain();
 
+    private RectF mTmpRectF;
+
+    // Variables holding previous states, used for detecting redundant calls and invalidation.
+    private Point mPrevStartCoordsOnScreen = new Point(
+            INEXISTENT_PREVIOUS_CONFIG_VALUE, INEXISTENT_PREVIOUS_CONFIG_VALUE);
+    private PointF mPrevCenterCoordsOnScreen = new PointF(
+            INEXISTENT_PREVIOUS_CONFIG_VALUE, INEXISTENT_PREVIOUS_CONFIG_VALUE);
+    private float mPrevScale = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+
     /**
      * Initializes a magnifier.
      *
@@ -90,19 +101,22 @@
     /**
      * Shows the magnifier on the screen.
      *
-     * @param centerXOnScreen horizontal coordinate of the center point of the magnifier source
-     * @param centerYOnScreen vertical coordinate of the center point of the magnifier source
-     * @param scale the scale at which the magnifier zooms on the source content
+     * @param centerXOnScreen horizontal coordinate of the center point of the magnifier source. The
+     *        lower end is clamped to 0
+     * @param centerYOnScreen vertical coordinate of the center point of the magnifier source. The
+     *        lower end is clamped to 0
+     * @param scale the scale at which the magnifier zooms on the source content. The
+     *        lower end is clamped to 1 and the higher end to 4
      */
     public void show(@FloatRange(from=0) float centerXOnScreen,
             @FloatRange(from=0) float centerYOnScreen,
-            @FloatRange(from=MINIMUM_MAGNIFIER_SCALE, to=MAXIMUM_MAGNIFIER_SCALE) float scale) {
-        if (scale > MAXIMUM_MAGNIFIER_SCALE) {
-            scale = MAXIMUM_MAGNIFIER_SCALE;
+            @FloatRange(from=1, to=4) float scale) {
+        if (scale > 4) {
+            scale = 4;
         }
 
-        if (scale < MINIMUM_MAGNIFIER_SCALE) {
-            scale = MINIMUM_MAGNIFIER_SCALE;
+        if (scale < 1) {
+            scale = 1;
         }
 
         if (centerXOnScreen < 0) {
@@ -113,9 +127,19 @@
             centerYOnScreen = 0;
         }
 
-        maybeResizeBitmap(scale);
+        showInternal(centerXOnScreen, centerYOnScreen, scale, false);
+    }
+
+    private void showInternal(@FloatRange(from=0) float centerXOnScreen,
+            @FloatRange(from=0) float centerYOnScreen,
+            @FloatRange(from=1, to=4) float scale,
+            boolean forceShow) {
+        if (mPrevScale != scale) {
+            resizeBitmap(scale);
+            mPrevScale = scale;
+        }
         configureCoordinates(centerXOnScreen, centerYOnScreen);
-        performPixelCopy();
+        maybePerformPixelCopy(scale, forceShow);
 
         if (mWindow.isShowing()) {
             mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
@@ -124,6 +148,9 @@
             mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY,
                     mWindowCoords.x, mWindowCoords.y);
         }
+
+        mPrevCenterCoordsOnScreen.x = centerXOnScreen;
+        mPrevCenterCoordsOnScreen.y = centerYOnScreen;
     }
 
     /**
@@ -131,6 +158,38 @@
      */
     public void dismiss() {
         mWindow.dismiss();
+
+        mPrevStartCoordsOnScreen.x = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+        mPrevStartCoordsOnScreen.y = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+        mPrevCenterCoordsOnScreen.x = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+        mPrevCenterCoordsOnScreen.y = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+        mPrevScale = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+    }
+
+    /**
+     * Forces the magnifier to update content by taking and showing a new snapshot using the
+     * previous coordinates. It does this only if the magnifier is showing and the dirty rectangle
+     * intersects the rectangle which holds the content to be magnified.
+     *
+     * @param dirtyRectOnScreen the rectangle representing the screen bounds of the dirty region
+     */
+    public void invalidate(RectF dirtyRectOnScreen) {
+        if (mWindow.isShowing() && mPrevCenterCoordsOnScreen.x != INEXISTENT_PREVIOUS_CONFIG_VALUE
+                && mPrevCenterCoordsOnScreen.y != INEXISTENT_PREVIOUS_CONFIG_VALUE
+                && mPrevScale != INEXISTENT_PREVIOUS_CONFIG_VALUE) {
+            // Update the current showing RectF.
+            mTmpRectF = new RectF(mPrevStartCoordsOnScreen.x,
+                    mPrevStartCoordsOnScreen.y,
+                    mPrevStartCoordsOnScreen.x + mBitmap.getWidth(),
+                    mPrevStartCoordsOnScreen.y + mBitmap.getHeight());
+
+            // Update only if we are currently showing content that has been declared as invalid.
+            if (RectF.intersects(dirtyRectOnScreen, mTmpRectF)) {
+                // Update the contents shown in the magnifier.
+                showInternal(mPrevCenterCoordsOnScreen.x, mPrevCenterCoordsOnScreen.y, mPrevScale,
+                        true /* forceShow */);
+            }
+        }
     }
 
     /**
@@ -147,13 +206,11 @@
         return mWindowWidth;
     }
 
-    private void maybeResizeBitmap(float scale) {
+    private void resizeBitmap(float scale) {
         final int bitmapWidth = (int) (mWindowWidth / scale);
         final int bitmapHeight = (int) (mWindowHeight / scale);
-        if (mBitmap.getWidth() != bitmapWidth || mBitmap.getHeight() != bitmapHeight) {
-            mBitmap.reconfigure(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
-            getImageView().setImageBitmap(mBitmap);
-        }
+        mBitmap.reconfigure(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+        getImageView().setImageBitmap(mBitmap);
     }
 
     private void configureCoordinates(float posXOnScreen, float posYOnScreen) {
@@ -166,16 +223,25 @@
         mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset;
     }
 
-    private void performPixelCopy() {
-        int startX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
+    private void maybePerformPixelCopy(final float scale, final boolean forceShow) {
+        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+        int rawStartX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
+
         // Clamp startX value to avoid distorting the rendering of the magnifier content.
-        if (startX < 0) {
-            startX = 0;
-        } else if (startX + mBitmap.getWidth() > mView.getWidth()) {
-            startX = mView.getWidth() - mBitmap.getWidth();
+        if (rawStartX < 0) {
+            rawStartX = 0;
+        } else if (rawStartX + mBitmap.getWidth() > mView.getWidth()) {
+            rawStartX = mView.getWidth() - mBitmap.getWidth();
         }
 
-        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+        if (!forceShow && rawStartX == mPrevStartCoordsOnScreen.x
+                && startY == mPrevStartCoordsOnScreen.y
+                && scale == mPrevScale) {
+            // Skip, we are already showing the desired content.
+            return;
+        }
+
+        final int startX = rawStartX;
         final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
 
         if (viewRootImpl != null && viewRootImpl.mSurface != null
@@ -185,7 +251,11 @@
                     new Rect(startX, startY, startX + mBitmap.getWidth(),
                             startY + mBitmap.getHeight()),
                     mBitmap,
-                    result -> getImageView().invalidate(),
+                    result -> {
+                        getImageView().invalidate();
+                        mPrevStartCoordsOnScreen.x = startX;
+                        mPrevStartCoordsOnScreen.y = startY;
+                    },
                     mPixelCopyHandler);
         } else {
             Log.d(LOG_TAG, "Could not perform PixelCopy request");