Always place notification dots directly on adaptive icon path

- Calculate point on icon path nearest to top right corner, and
  use that as center for the dot
- Cleanup code related to dot offset

Test:
Set each style (different icon shape) and verify dot is in correct
placement for each of:
- Folders
- Icons in folders
- Icons in all apps
- Icons on workspace

Bug: 124414511
Change-Id: I036ed3677e8af222f00d4fad4a36a7e4d9b49ad9
diff --git a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
index 6bc859a..af07aa3 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
@@ -23,8 +23,10 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
-import android.graphics.Point;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.util.Log;
 import android.view.ViewDebug;
 
@@ -36,33 +38,51 @@
     private static final String TAG = "DotRenderer";
 
     // The dot size is defined as a percentage of the app icon size.
-    private static final float SIZE_PERCENTAGE = 0.38f;
+    private static final float SIZE_PERCENTAGE = 0.228f;
 
-    // Extra scale down of the dot
-    private static final float DOT_SCALE = 0.6f;
-
-    // Offset the dot slightly away from the icon if there's space.
-    private static final float OFFSET_PERCENTAGE = 0.02f;
-
-    private final float mDotCenterOffset;
-    private final int mOffset;
     private final float mCircleRadius;
     private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
 
     private final Bitmap mBackgroundWithShadow;
     private final float mBitmapOffset;
 
-    public DotRenderer(int iconSizePx) {
-        mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx;
-        mOffset = (int) (OFFSET_PERCENTAGE * iconSizePx);
+    // Stores the center x and y position as a percentage (0 to 1) of the icon size
+    private final float[] mRightDotPosition;
+    private final float[] mLeftDotPosition;
 
-        int size = (int) (DOT_SCALE * mDotCenterOffset);
+    public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
+        int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
         ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT);
         builder.ambientShadowAlpha = 88;
         mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);
         mCircleRadius = builder.radius;
 
         mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width.
+
+        // Find the points on the path that are closest to the top left and right corners.
+        mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1);
+        mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1);
+    }
+
+    private static float[] getPathPoint(Path path, float size, float direction) {
+        float halfSize = size / 2;
+        // Small delta so that we don't get a zero size triangle
+        float delta = 1;
+
+        float x = halfSize + direction * halfSize;
+        Path trianglePath = new Path();
+        trianglePath.moveTo(halfSize, halfSize);
+        trianglePath.lineTo(x + delta * direction, 0);
+        trianglePath.lineTo(x, -delta);
+        trianglePath.close();
+
+        trianglePath.op(path, Path.Op.INTERSECT);
+        float[] pos = new float[2];
+        new PathMeasure(trianglePath, false).getPosTan(0, pos, null);
+
+        pos[0] = pos[0] / size;
+        pos[1] = pos[1] / size;
+        return pos;
     }
 
     /**
@@ -74,15 +94,21 @@
             return;
         }
         canvas.save();
-        // We draw the dot relative to its center.
-        float dotCenterX = params.leftAlign
-                ? params.iconBounds.left + mDotCenterOffset / 2
-                : params.iconBounds.right - mDotCenterOffset / 2;
-        float dotCenterY = params.iconBounds.top + mDotCenterOffset / 2;
 
-        int offsetX = Math.min(mOffset, params.spaceForOffset.x);
-        int offsetY = Math.min(mOffset, params.spaceForOffset.y);
-        canvas.translate(dotCenterX + offsetX, dotCenterY - offsetY);
+        Rect iconBounds = params.iconBounds;
+        float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition;
+        float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0];
+        float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1];
+
+        // Ensure dot fits entirely in canvas clip bounds.
+        Rect canvasBounds = canvas.getClipBounds();
+        float offsetX = params.leftAlign
+                ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset))
+                : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset));
+        float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset));
+
+        // We draw the dot relative to its center.
+        canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY);
         canvas.scale(params.scale, params.scale);
 
         mCirclePaint.setColor(Color.BLACK);
@@ -102,9 +128,6 @@
         /** The progress of the animation, from 0 to 1. */
         @ViewDebug.ExportedProperty(category = "notification dot")
         public float scale;
-        /** Overrides internally calculated offset if specified value is smaller. */
-        @ViewDebug.ExportedProperty(category = "notification dot")
-        public Point spaceForOffset = new Point();
         /** Whether the dot should align to the top left of the icon rather than the top right. */
         @ViewDebug.ExportedProperty(category = "notification dot")
         public boolean leftAlign;
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 8291acc..bff7f42 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -46,10 +46,12 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconCache.IconLoadRequest;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.views.ActivityContext;
 
@@ -388,7 +390,7 @@
     protected void drawDotIfNecessary(Canvas canvas) {
         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
             getIconBounds(mDotParams.iconBounds);
-            mDotParams.spaceForOffset.set((getWidth() - mIconSize) / 2, getPaddingTop());
+            Utilities.scaleRectAboutCenter(mDotParams.iconBounds, IconShape.getNormalizationScale());
             final int scrollX = getScrollX();
             final int scrollY = getScrollY();
             canvas.translate(scrollX, scrollY);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index c0affb9..3d2d7cf 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,15 +24,13 @@
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.view.Surface;
-import android.view.View;
 import android.view.WindowManager;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
@@ -240,7 +238,8 @@
         updateWorkspacePadding();
 
         // This is done last, after iconSizePx is calculated above.
-        mDotRenderer = new DotRenderer(iconSizePx);
+        mDotRenderer = new DotRenderer(iconSizePx, IconShape.getShapePath(),
+                IconShape.DEFAULT_PATH_SIZE);
     }
 
     public DeviceProfile copy(Context context) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 6fa9ba9..8d9c520 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -36,6 +36,8 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.Alarm;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
@@ -50,11 +52,11 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.OnAlarmListener;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.SimpleOnStylusPressListener;
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.BaseItemDragListener;
@@ -68,8 +70,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.NonNull;
-
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
@@ -510,10 +510,11 @@
             Rect iconBounds = mDotParams.iconBounds;
             BubbleTextView.getIconBounds(this, iconBounds,
                     mLauncher.getWallpaperDeviceProfile().iconSizePx);
+            float iconScale = (float) mBackground.previewSize / iconBounds.width();
+            Utilities.scaleRectAboutCenter(iconBounds, iconScale);
 
             // If we are animating to the accepting state, animate the dot out.
             mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
-            mDotParams.spaceForOffset.set(getWidth() - iconBounds.right, iconBounds.top);
             mDotParams.color = mBackground.getDotColor();
             mDotRenderer.draw(canvas, mDotParams);
         }