From 4c261f7343f0b6db491f31ec626c91ddec597790 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 27 Jan 2025 23:30:06 -0800 Subject: [PATCH] Fixing icon shape not properly clipped > Using ShapeDelegate for icon clipping > Creating ShapeDelegate based on the current icon shape > Defining different shape delegates for icon and folder > Updating preview rendering to use dagger graph > Adding tests for all icon shapes Bug: 392145015 Test: atest IconShapesProviderTest Flag: com.android.launcher3.enable_launcher_icon_shapes Change-Id: Ic9f287e2a6fef2aad18ba224404de58a64da0bb3 --- .../uioverrides/PredictedAppIcon.java | 2 +- src/com/android/launcher3/DeviceProfile.java | 2 +- src/com/android/launcher3/LauncherPrefs.kt | 21 ++- .../android/launcher3/dragndrop/DragView.java | 6 +- .../dragndrop/FolderAdaptiveIcon.java | 2 +- .../folder/FolderAnimationManager.java | 2 +- .../launcher3/folder/PreviewBackground.java | 2 +- .../graphics/GridCustomizationsProvider.java | 32 ++--- .../android/launcher3/graphics/IconShape.kt | 125 ++++++++++-------- .../graphics/LauncherPreviewRenderer.java | 34 +++++ .../graphics/PreloadIconDrawable.java | 2 +- .../graphics/PreviewSurfaceRenderer.java | 23 ++-- .../launcher3/graphics/ThemeManager.kt | 25 ++-- .../launcher3/icons/LauncherIcons.java | 21 +-- .../launcher3/shapes/IconShapeModel.kt | 7 +- .../launcher3/shapes/IconShapesProvider.kt | 6 +- .../shapes/IconShapesProviderTest.kt | 90 +++++++++++++ 17 files changed, 271 insertions(+), 131 deletions(-) create mode 100644 tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index a85e5e0049..a44fde73f1 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -142,7 +142,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { int shadowSize = context.getResources().getDimensionPixelSize( R.dimen.blur_size_thin_outline); mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER); - mShapePath = IconShape.INSTANCE.get(context).getShapeOverridePath(mNormalizedIconSize); + mShapePath = IconShape.INSTANCE.get(context).getShape().getPath(mNormalizedIconSize); } @Override diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 9f47da7ca3..b4a24f19df 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -873,7 +873,7 @@ public class DeviceProfile { if (renderer == null) { renderer = new DotRenderer( size, - IconShape.INSTANCE.get(context).getShapeOverridePath(DEFAULT_DOT_SIZE), + IconShape.INSTANCE.get(context).getShape().getPath(DEFAULT_DOT_SIZE), DEFAULT_DOT_SIZE); cache.put(size, renderer); } diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt index d8bb84e6c0..484cef4da5 100644 --- a/src/com/android/launcher3/LauncherPrefs.kt +++ b/src/com/android/launcher3/LauncherPrefs.kt @@ -58,11 +58,11 @@ constructor(@ApplicationContext private val encryptedContext: Context) { else encryptedContext.getSharedPreferences(sharedPrefFile, MODE_PRIVATE) /** Returns the value with type [T] for [item]. */ - fun get(item: ContextualItem): T = + open fun get(item: ContextualItem): T = getInner(item, item.defaultValueFromContext(encryptedContext)) /** Returns the value with type [T] for [item]. */ - fun get(item: ConstantItem): T = getInner(item, item.defaultValue) + open fun get(item: ConstantItem): T = getInner(item, item.defaultValue) /** * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the @@ -406,3 +406,20 @@ enum class EncryptionType { ENCRYPTED, DEVICE_PROTECTED, } + +/** + * LauncherPrefs which delegates all lookup to [prefs] but uses the real prefs for initial values + */ +class ProxyPrefs(context: Context, private val prefs: SharedPreferences) : LauncherPrefs(context) { + + private val realPrefs = LauncherPrefs(context) + + override val Item.sharedPrefs: SharedPreferences + get() = prefs + + override fun get(item: ConstantItem) = + super.get(backedUpItem(item.sharedPrefKey, realPrefs.get(item))) + + override fun get(item: ContextualItem) = + super.get(backedUpItem(item.sharedPrefKey, realPrefs.get(item))) +} diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index 9c82748cde..072673d1ea 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -273,7 +273,11 @@ public abstract class DragView extends Fram Rect shrunkBounds = new Rect(bounds); Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f); adaptiveIcon.setBounds(shrunkBounds); - final Path mask = IconShape.INSTANCE.get(getContext()).getShapeOverridePath(w); + + IconShape iconShape = IconShape.INSTANCE.get(getContext()); + final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon + ? iconShape.getFolderShape() : iconShape.getShape()) + .getPath(shrunkBounds); mTranslateX = new SpringFloatValue(DragView.this, w * AdaptiveIconDrawable.getExtraInsetFraction()); diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java index 6a43b24366..929e52e1fc 100644 --- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java +++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java @@ -165,7 +165,7 @@ public class FolderAdaptiveIcon extends AdaptiveIconDrawable { Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); backgroundPaint.setColor(bg.getBgColor()); bg.drawShadow(backgroundCanvas); - backgroundCanvas.drawCircle(size / 2f, size / 2f, bg.getRadius(), backgroundPaint); + backgroundCanvas.drawPaint(backgroundPaint); bg.drawBackgroundStroke(backgroundCanvas); } diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java index 588a6dbdae..d2ff2cb040 100644 --- a/src/com/android/launcher3/folder/FolderAnimationManager.java +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -233,7 +233,7 @@ public class FolderAnimationManager { } play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration); - ShapeDelegate shapeDelegate = IconShape.INSTANCE.get(mContext).getShape(); + ShapeDelegate shapeDelegate = IconShape.INSTANCE.get(mContext).getFolderShape(); // Create reveal animator for the folder background play(a, shapeDelegate.createRevealAnimator( mFolder, startRect, endRect, finalRadius, !mIsOpening)); diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java index df41d4786c..d9c60db9f6 100644 --- a/src/com/android/launcher3/folder/PreviewBackground.java +++ b/src/com/android/launcher3/folder/PreviewBackground.java @@ -261,7 +261,7 @@ public class PreviewBackground extends DelegatedCellDrawing { } private ShapeDelegate getShape() { - return IconShape.INSTANCE.get(mContext).getShape(); + return IconShape.INSTANCE.get(mContext).getFolderShape(); } public void drawShadow(Canvas canvas) { diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java index ad176dc3d5..3479cec97a 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java @@ -16,10 +16,12 @@ package com.android.launcher3.graphics; -import static com.android.launcher3.graphics.IconShape.PREF_ICON_SHAPE; +import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static java.util.Objects.requireNonNullElse; + import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -52,7 +54,6 @@ import com.android.launcher3.util.RunnableList; import com.android.systemui.shared.Flags; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ExecutionException; @@ -141,23 +142,15 @@ public class GridCustomizationsProvider extends ContentProvider { if (Flags.newCustomizationPickerUi()) { MatrixCursor cursor = new MatrixCursor(new String[]{ KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT}); - List shapes = IconShapesProvider.INSTANCE.getShapes() - .values() - .stream() - .toList(); - String currentPath = LauncherPrefs.get(context).get(PREF_ICON_SHAPE); - IconShapeModel currentShape = shapes.stream() - .filter(shape -> currentPath.equals(shape.getPathString())) - .findFirst() - .orElse(IconShapesProvider.INSTANCE.getShapes().get("circle")); - - for (int i = 0; i < shapes.size(); i++) { - IconShapeModel shape = shapes.get(i); + String currentShapePath = + ThemeManager.INSTANCE.get(context).getIconState().getIconMask(); + for (IconShapeModel shape : IconShapesProvider.INSTANCE.getShapes().values()) { cursor.newRow() .add(KEY_SHAPE_KEY, shape.getKey()) .add(KEY_SHAPE_TITLE, shape.getTitle()) .add(KEY_PATH, shape.getPathString()) - .add(KEY_IS_DEFAULT, shape.equals(currentShape)); + .add(KEY_IS_DEFAULT, + shape.getPathString().equals(currentShapePath)); } return cursor; } else { @@ -218,9 +211,8 @@ public class GridCustomizationsProvider extends ContentProvider { switch (path) { case KEY_DEFAULT_GRID: { if (Flags.newCustomizationPickerUi()) { - String shapeKey = values.getAsString(KEY_SHAPE_KEY); - IconShapeModel shape = IconShapesProvider.INSTANCE.getShapes().get(shapeKey); - IconShape.INSTANCE.get(context).setShapeOverride(shape); + LauncherPrefs.INSTANCE.get(context).put(PREF_ICON_SHAPE, + requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), "")); } String gridName = values.getAsString(KEY_NAME); InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); @@ -362,9 +354,7 @@ public class GridCustomizationsProvider extends ContentProvider { if (Flags.newCustomizationPickerUi() && com.android.launcher3.Flags.enableLauncherIconShapes()) { String shapeKey = message.getData().getString(KEY_SHAPE_KEY); - IconShapeModel shape = - IconShapesProvider.INSTANCE.getShapes().get(shapeKey); - renderer.updateShape(shape); + renderer.updateShape(shapeKey); } break; case MESSAGE_ID_UPDATE_GRID: diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/IconShape.kt index 2c4d8e4de3..0938a5a9bb 100644 --- a/src/com/android/launcher3/graphics/IconShape.kt +++ b/src/com/android/launcher3/graphics/IconShape.kt @@ -18,7 +18,6 @@ package com.android.launcher3.graphics import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator -import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix @@ -41,19 +40,12 @@ import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.SvgPathParser import androidx.graphics.shapes.toPath import androidx.graphics.shapes.transformed -import com.android.launcher3.EncryptionType -import com.android.launcher3.LauncherPrefs -import com.android.launcher3.LauncherPrefs.Companion.backedUpItem import com.android.launcher3.anim.RoundedRectRevealOutlineProvider -import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.dagger.LauncherAppComponent import com.android.launcher3.dagger.LauncherAppSingleton -import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener import com.android.launcher3.icons.GraphicsUtils -import com.android.launcher3.icons.IconNormalizer -import com.android.launcher3.shapes.IconShapeModel -import com.android.launcher3.shapes.IconShapesProvider +import com.android.launcher3.icons.IconNormalizer.normalizeAdaptiveIcon import com.android.launcher3.util.DaggerSingletonObject import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.views.ClipPathView @@ -63,56 +55,52 @@ import javax.inject.Inject @LauncherAppSingleton class IconShape @Inject -constructor( - @ApplicationContext private val context: Context, - private val prefs: LauncherPrefs, - private val themeManager: ThemeManager, - lifeCycle: DaggerSingletonTracker, -) { +constructor(private val themeManager: ThemeManager, lifeCycle: DaggerSingletonTracker) { - var shapeOverride: IconShapeModel? = getShapeFromPathString(prefs.get(PREF_ICON_SHAPE)) - set(value) { - field = value - if (context !is PreviewContext) { - value?.let { prefs.put(PREF_ICON_SHAPE, value.pathString) } - } - } + val normalizationScale = + normalizeAdaptiveIcon( + AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)), + AREA_CALC_SIZE, + ) - var normalizationScale: Float = IconNormalizer.ICON_VISIBLE_AREA_FACTOR + var shape: ShapeDelegate = pickBestShape(themeManager.iconState.iconMask) private set - var shape: ShapeDelegate = pickBestShape(themeManager) + var folderShape: ShapeDelegate = + themeManager.iconState.run { + if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape + else pickBestShape(folderShapeMask) + } private set init { - val changeListener = ThemeChangeListener { shape = pickBestShape(themeManager) } + val changeListener = ThemeChangeListener { + shape = pickBestShape(themeManager.iconState.iconMask) + folderShape = + themeManager.iconState.run { + if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape + else pickBestShape(folderShapeMask) + } + } themeManager.addChangeListener(changeListener) lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) } } - fun getShapeOverridePath(pathSize: Float = DEFAULT_PATH_SIZE): Path { - val path = PathParser.createPathFromPathData(themeManager.iconState.iconMask) - if (pathSize != DEFAULT_PATH_SIZE) { - val matrix = Matrix() - val scale: Float = pathSize / DEFAULT_PATH_SIZE - matrix.setScale(scale, scale) - path.transform(matrix) - } - return path - } + interface ShapeDelegate { + fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) = + Path().apply { addToPath(this, 0f, 0f, pathSize / 2) } - /** Initializes the shape which is closest to the [AdaptiveIconDrawable] */ - private fun pickBestShape(themeManager: ThemeManager): ShapeDelegate { - val drawable = - AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)).apply { - setBounds(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE) + fun getPath(bounds: Rect) = + Path().apply { + addToPath( + this, + bounds.left.toFloat(), + bounds.top.toFloat(), + // Radius is half of the average size of the icon + (bounds.width() + bounds.height()) / 4f, + ) } - normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, AREA_CALC_SIZE) - return pickBestShape(drawable.iconMask, themeManager.iconState.iconMask) - } - - interface ShapeDelegate { fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint) fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) @@ -217,14 +205,24 @@ constructor( paint: Paint, ) { tmpPath.reset() - addToPath(tmpPath, offsetX, offsetY, radius) + addToPath(tmpPath, offsetX, offsetY, radius, tmpMatrix) canvas.drawPath(tmpPath, paint) } override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) { - tmpMatrix.setScale(radius / 50, radius / 50) - tmpMatrix.postTranslate(offsetX, offsetY) - basePath.transform(tmpMatrix, path) + addToPath(path, offsetX, offsetY, radius, Matrix()) + } + + private fun addToPath( + path: Path, + offsetX: Float, + offsetY: Float, + radius: Float, + matrix: Matrix, + ) { + matrix.setScale(radius / 50, radius / 50) + matrix.postTranslate(offsetX, offsetY) + basePath.transform(matrix, path) } override fun createRevealAnimator( @@ -292,20 +290,11 @@ constructor( @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape) const val TAG = "IconShape" - const val KEY_ICON_SHAPE = "icon_shape" const val DEFAULT_PATH_SIZE = 100f const val AREA_CALC_SIZE = 1000 // .1% error margin const val AREA_DIFF_THRESHOLD = AREA_CALC_SIZE * AREA_CALC_SIZE / 1000 - @JvmField val PREF_ICON_SHAPE = backedUpItem(KEY_ICON_SHAPE, "", EncryptionType.ENCRYPTED) - - private fun getShapeFromPathString(pathString: String): IconShapeModel? { - return IconShapesProvider.shapes.values.firstOrNull { shape: IconShapeModel -> - shape.pathString == pathString - } - } - /** Returns a function to calculate area diff from [base] */ @VisibleForTesting fun areaDiffCalculator(base: Path): (ShapeDelegate) -> Int { @@ -323,6 +312,26 @@ constructor( } } + @VisibleForTesting + fun pickBestShape(shapeStr: String): ShapeDelegate { + val baseShape = + if (shapeStr.isNotEmpty()) { + PathParser.createPathFromPathData(shapeStr).apply { + transform( + Matrix().apply { + setScale(AREA_CALC_SIZE / 100f, AREA_CALC_SIZE / 100f) + } + ) + } + } else { + AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)).let { + it.setBounds(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE) + it.iconMask + } + } + return pickBestShape(baseShape, shapeStr) + } + @VisibleForTesting fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate { val calcAreaDiff = areaDiffCalculator(baseShape) diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index a0b73ae184..911064cbc0 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -67,7 +67,9 @@ import com.android.launcher3.Hotseat; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.ProxyPrefs; import com.android.launcher3.R; import com.android.launcher3.Workspace; import com.android.launcher3.WorkspaceLayoutManager; @@ -75,6 +77,9 @@ import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.celllayout.CellPosMapper; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppModule; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.FixedContainerItems; @@ -100,12 +105,16 @@ import com.android.launcher3.widget.LocalColorExtractor; import com.android.launcher3.widget.util.WidgetSizes; import com.android.systemui.shared.Flags; +import dagger.BindsInstance; +import dagger.Component; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -126,12 +135,25 @@ public class LauncherPreviewRenderer extends ContextWrapper */ public static class PreviewContext extends SandboxContext { + private final String mPrefName; + public PreviewContext(Context base, InvariantDeviceProfile idp) { super(base); + mPrefName = "preview-" + UUID.randomUUID().toString(); + initDaggerComponent(DaggerLauncherPreviewRenderer_PreviewAppComponent.builder() + .bindPrefs(new ProxyPrefs( + this, getSharedPreferences(mPrefName, MODE_PRIVATE)))); + putObject(InvariantDeviceProfile.INSTANCE, idp); putObject(LauncherAppState.INSTANCE, new LauncherAppState(this, null /* iconCacheFileName */)); } + + @Override + protected void cleanUpObjects() { + super.cleanUpObjects(); + deleteSharedPreferences(mPrefName); + } } private final List mDpChangeListeners = new ArrayList<>(); @@ -578,4 +600,16 @@ public class LauncherPreviewRenderer extends ContextWrapper return true; } } + + @LauncherAppSingleton + @Component(modules = LauncherAppModule.class) + public interface PreviewAppComponent extends LauncherAppComponent { + + /** Builder for NexusLauncherAppComponent. */ + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindPrefs(LauncherPrefs prefs); + PreviewAppComponent build(); + } + } } diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java index 3464e9bdb6..7da4e65ca7 100644 --- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -120,7 +120,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable { IconPalette.getPreloadProgressColor(context, info.bitmap.color), getPreloadColors(context), Utilities.isDarkTheme(context), - IconShape.INSTANCE.get(context).getShapeOverridePath(DEFAULT_PATH_SIZE) + IconShape.INSTANCE.get(context).getShape().getPath(DEFAULT_PATH_SIZE) ); } diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index 6afac7148c..a4b681b06d 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; +import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -52,6 +53,7 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Workspace; import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext; @@ -62,7 +64,6 @@ import com.android.launcher3.model.GridSizeMigrationDBController; import com.android.launcher3.model.LoaderTask; import com.android.launcher3.model.ModelDbController; import com.android.launcher3.provider.LauncherDbUtils; -import com.android.launcher3.shapes.IconShapeModel; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.Themes; @@ -71,6 +72,7 @@ import com.android.systemui.shared.Flags; import java.util.ArrayList; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** Render preview using surface view. */ @@ -91,7 +93,8 @@ public class PreviewSurfaceRenderer { private Context mContext; private SparseIntArray mPreviewColorOverride; private String mGridName; - private IconShapeModel mShape; + private String mShapeKey; + @Nullable private Boolean mDarkMode; private boolean mDestroyed = false; private LauncherPreviewRenderer mRenderer; @@ -220,14 +223,14 @@ public class PreviewSurfaceRenderer { /** * Update the shapes of the launcher preview * - * @param shape path for shapes + * @param shapeKey key for the IconShape model */ - public void updateShape(@NonNull IconShapeModel shape) { - if (shape.equals(mShape)) { - Log.w(TAG, "Preview shape already set, skipping. shape=" + shape); + public void updateShape(@Nullable String shapeKey) { + if (Objects.equals(mShapeKey, shapeKey)) { + Log.w(TAG, "Preview shape already set, skipping. shape=" + mShapeKey); return; } - mShape = shape; + mShapeKey = shapeKey; loadAsync(); } @@ -317,11 +320,11 @@ public class PreviewSurfaceRenderer { final Context inflationContext = getPreviewContext(); final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName); if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp) - || mShape != null) { + || mShapeKey != null) { // Start the migration PreviewContext previewContext = new PreviewContext(inflationContext, idp); - if (mShape != null) { - IconShape.INSTANCE.get(previewContext).setShapeOverride(mShape); + if (mShapeKey != null) { + LauncherPrefs.INSTANCE.get(previewContext).put(PREF_ICON_SHAPE, mShapeKey); } // Copy existing data to preview DB LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext) diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt index 989471f4ab..f24c2ab4c2 100644 --- a/src/com/android/launcher3/graphics/ThemeManager.kt +++ b/src/com/android/launcher3/graphics/ThemeManager.kt @@ -25,10 +25,9 @@ import com.android.launcher3.LauncherPrefs.Companion.backedUpItem import com.android.launcher3.dagger.ApplicationContext import com.android.launcher3.dagger.LauncherAppComponent import com.android.launcher3.dagger.LauncherAppSingleton -import com.android.launcher3.graphics.IconShape.Companion.KEY_ICON_SHAPE -import com.android.launcher3.graphics.IconShape.Companion.PREF_ICON_SHAPE import com.android.launcher3.icons.IconThemeController import com.android.launcher3.icons.mono.MonoIconThemeController +import com.android.launcher3.shapes.IconShapesProvider import com.android.launcher3.util.DaggerSingletonObject import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.util.Executors.MAIN_EXECUTOR @@ -93,20 +92,26 @@ constructor( fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener) private fun parseIconState(): IconState { - val shapeOverride = prefs.get(PREF_ICON_SHAPE) + val shapeModel = + prefs.get(PREF_ICON_SHAPE).let { shapeOverride -> + IconShapesProvider.shapes.values.firstOrNull { it.key == shapeOverride } + } + val iconMask = + when { + shapeModel != null -> shapeModel.pathString + CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> "" + else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID) + } return IconState( - iconMask = - when { - shapeOverride.isNotEmpty() -> shapeOverride - CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> "" - else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID) - }, + iconMask = iconMask, + folderShapeMask = shapeModel?.folderPathString ?: iconMask, isMonoTheme = isMonoThemeEnabled, ) } data class IconState( val iconMask: String, + val folderShapeMask: String, val isMonoTheme: Boolean, val themeCode: String = if (isMonoTheme) "with-theme" else "no-theme", ) { @@ -121,9 +126,11 @@ constructor( companion object { @JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager) + const val KEY_ICON_SHAPE = "icon_shape_model" const val KEY_THEMED_ICONS = "themed_icons" @JvmField val THEMED_ICONS = backedUpItem(KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED) + @JvmField val PREF_ICON_SHAPE = backedUpItem(KEY_ICON_SHAPE, "", EncryptionType.ENCRYPTED) private const val ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED" private val CONFIG_ICON_MASK_RES_ID: Int = diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java index 2ffbeb8097..59fff62d06 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -17,22 +17,18 @@ package com.android.launcher3.icons; import android.content.Context; -import android.graphics.Matrix; import android.graphics.Path; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; import android.os.UserHandle; import androidx.annotation.NonNull; -import androidx.core.graphics.PathParser; import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.graphics.IconShape; import com.android.launcher3.graphics.ThemeManager; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.shapes.IconShapeModel; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.UserIconInfo; @@ -87,22 +83,7 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { @Override public Path getShapePath(AdaptiveIconDrawable drawable, Rect iconBounds) { if (!Flags.enableLauncherIconShapes()) return drawable.getIconMask(); - - IconShapeModel shapeOverride = IconShape.INSTANCE.get(mContext).getShapeOverride(); - if (shapeOverride != null) { - Path maskPath = PathParser.createPathFromPathData(shapeOverride.getPathString()); - Matrix matrix = new Matrix(); - // Assuming Path is in [0, 0, 100, 100] coordinate space. - matrix.setRectToRect( - new RectF(0, 0, 100, 100), - new RectF(iconBounds), - Matrix.ScaleToFit.CENTER // Todo: CENTER or FILL? - ); - maskPath.transform(matrix); - return maskPath; - } else { - return drawable.getIconMask(); - } + return IconShape.INSTANCE.get(mContext).getShape().getPath(iconBounds); } @Override diff --git a/src/com/android/launcher3/shapes/IconShapeModel.kt b/src/com/android/launcher3/shapes/IconShapeModel.kt index 10b7f91f85..dd6c4321bf 100644 --- a/src/com/android/launcher3/shapes/IconShapeModel.kt +++ b/src/com/android/launcher3/shapes/IconShapeModel.kt @@ -16,4 +16,9 @@ package com.android.launcher3.shapes -data class IconShapeModel(val key: String, val title: String, val pathString: String) +data class IconShapeModel( + val key: String, + val title: String, + val pathString: String, + val folderPathString: String = pathString, +) diff --git a/src/com/android/launcher3/shapes/IconShapesProvider.kt b/src/com/android/launcher3/shapes/IconShapesProvider.kt index a190e22774..8608437d11 100644 --- a/src/com/android/launcher3/shapes/IconShapesProvider.kt +++ b/src/com/android/launcher3/shapes/IconShapesProvider.kt @@ -28,7 +28,7 @@ object IconShapesProvider { key = "arch", title = "arch", pathString = - "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z", + "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z", ), "4_sided_cookie" to IconShapeModel( @@ -42,7 +42,7 @@ object IconShapesProvider { key = "seven_sided_cookie", title = "7 sided cookie", pathString = - "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z", + "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z", ), "sunny" to IconShapeModel( @@ -62,7 +62,7 @@ object IconShapesProvider { key = "square", title = "square", pathString = - "M53.689 0.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82Z", + "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z", ), ) } else { diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt new file mode 100644 index 0000000000..234e050ea4 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.shapes + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.core.graphics.PathParser +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES +import com.android.launcher3.graphics.IconShape.GenericPathShape +import com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class IconShapesProviderTest { + + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path arch`() { + IconShapesProvider.shapes["arch"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path 4_sided_cookie`() { + IconShapesProvider.shapes["4_sided_cookie"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path seven_sided_cookie`() { + IconShapesProvider.shapes["seven_sided_cookie"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path sunny`() { + IconShapesProvider.shapes["sunny"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path circle`() { + IconShapesProvider.shapes["circle"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI) + fun `verify valid path square`() { + IconShapesProvider.shapes["square"]?.apply { + GenericPathShape(pathString) + PathParser.createPathFromPathData(pathString) + } + } +}