diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java index ce3082c4ac..b54ca8abbd 100644 --- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -53,6 +53,7 @@ import com.android.launcher3.util.window.RefreshRateTracker; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory; import com.android.launcher3.widget.custom.CustomWidgetManager; +import com.android.launcher3.widget.util.WidgetSizeHandler; import dagger.BindsInstance; @@ -101,8 +102,9 @@ public interface LauncherBaseAppComponent { InstantAppResolver getInstantAppResolver(); DumpManager getDumpManager(); StatsLogManager.StatsLogManagerFactory getStatsLogManagerFactory(); - ActivityContextComponent.Builder getActivityContextComponentBuilder(); - WidgetPickerComposeWrapper getWidgetPickerComposeWrapper(); + ActivityContextComponent.Builder getActivityContextComponentBuilder(); + WidgetPickerComposeWrapper getWidgetPickerComposeWrapper(); + WidgetSizeHandler getWidgetSizeHandler(); /** Builder for LauncherBaseAppComponent. */ diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java index 2b22aa57df..132591c70e 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProxy.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java @@ -419,9 +419,6 @@ public class GridCustomizationsProxy implements ProxyProvider { this.lifeCycleTracker = lifeCycleTracker; this.renderer = renderer; lifeCycleTracker.add(() -> destroyed = true); - // Preview grid change currently affects actual widget size. Revert grid changes - // when preview is destroyed to make sure Launcher widgets display correctly. - lifeCycleTracker.add(() -> renderer.updateGrid(null)); } @Override diff --git a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java index 3cf4d57ebc..487cb6417f 100644 --- a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java @@ -116,7 +116,8 @@ public class LauncherPreviewRenderer extends BaseContext super(context, Themes.getActivityThemeRes(context)); mUiHandler = new Handler(Looper.getMainLooper()); mIdp = idp; - mDp = getDeviceProfileForPreview(context).toBuilder(context).build(); + mDp = getDeviceProfileForPreview(context).toBuilder(context) + .setViewScaleProvider(new PreviewScaleProvider(this)).build(); mInsets = getInsets(context); mDp.updateInsets(mInsets); diff --git a/src/com/android/launcher3/preview/PreviewContext.kt b/src/com/android/launcher3/preview/PreviewContext.kt index 7ead5c7e54..2d6bdb33c6 100644 --- a/src/com/android/launcher3/preview/PreviewContext.kt +++ b/src/com/android/launcher3/preview/PreviewContext.kt @@ -15,6 +15,7 @@ */ package com.android.launcher3.preview +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.text.TextUtils import com.android.launcher3.InvariantDeviceProfile @@ -29,7 +30,6 @@ import com.android.launcher3.dagger.AppModule import com.android.launcher3.dagger.LauncherAppComponent import com.android.launcher3.dagger.LauncherAppSingleton import com.android.launcher3.dagger.LauncherComponentProvider.appComponent -import com.android.launcher3.dagger.LauncherComponentProvider.get import com.android.launcher3.dagger.LauncherConcurrencyModule import com.android.launcher3.dagger.PerDisplayModule import com.android.launcher3.dagger.PluginManagerWrapperModule @@ -47,6 +47,7 @@ import com.android.launcher3.util.SandboxContext import com.android.launcher3.util.dagger.LauncherExecutorsModule import com.android.launcher3.widget.LauncherWidgetHolder import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory +import com.android.launcher3.widget.util.WidgetSizeHandler import com.android.systemui.shared.Flags import dagger.BindsInstance import dagger.Component @@ -90,19 +91,21 @@ constructor( else selectionForWorkspaceScreen(workspacePageId) val builder = DaggerPreviewContext_PreviewAppComponent.builder().bindPrefs(prefs) - builder.bindLoaderParams( - LoaderParams( - workspaceSelection = selectionQuery, - sanitizeData = false, - loadNonWorkspaceItems = false, + builder + .bindLoaderParams( + LoaderParams( + workspaceSelection = selectionQuery, + sanitizeData = false, + loadNonWorkspaceItems = false, + ) ) - ) + .bindWidgetSizeHandler(NoOpWidgetSizeHandler(this)) if (layoutXml.isNullOrEmpty() || !Flags.extendibleThemeManager()) { mDbDir = null builder .bindParserFactory(LayoutParserFactory(this)) - .bindWidgetsFactory(get(base).widgetHolderFactory) + .bindWidgetsFactory(base.appComponent.widgetHolderFactory) } else { mDbDir = File(base.filesDir, randomUid) emptyDbDir() @@ -139,8 +142,19 @@ constructor( } } - override fun getDatabasePath(name: String): File { - return if (mDbDir != null) File(mDbDir, name) else super.getDatabasePath(name) + override fun getDatabasePath(name: String): File = + if (mDbDir != null) File(mDbDir, name) else super.getDatabasePath(name) + + private class NoOpWidgetSizeHandler(context: Context) : WidgetSizeHandler(context) { + + override fun updateSizeRangesAsync( + widgetId: Int, + info: AppWidgetProviderInfo, + spanX: Int, + spanY: Int, + ) { + // Ignore + } } @LauncherAppSingleton // Exclude widget module since we bind widget holder separately @@ -175,6 +189,8 @@ constructor( @BindsInstance fun bindLoaderParams(params: LoaderParams): Builder + @BindsInstance fun bindWidgetSizeHandler(handler: WidgetSizeHandler): Builder + override fun build(): PreviewAppComponent } } diff --git a/src/com/android/launcher3/preview/PreviewScaleProvider.kt b/src/com/android/launcher3/preview/PreviewScaleProvider.kt new file mode 100644 index 0000000000..5cbf5df3fd --- /dev/null +++ b/src/com/android/launcher3/preview/PreviewScaleProvider.kt @@ -0,0 +1,74 @@ +/* + * 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.preview + +import android.appwidget.AppWidgetManager +import android.graphics.PointF +import android.util.SizeF +import com.android.launcher3.DeviceProfile.DEFAULT_SCALE +import com.android.launcher3.DeviceProfile.ViewScaleProvider +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.widget.util.WidgetSizeHandler.Companion.getWidgetSizeList +import com.android.launcher3.widget.util.WidgetSizes +import kotlin.math.pow + +/** A [ViewScaleProvider] which scales widgets based on existing widget size */ +class PreviewScaleProvider(val target: ActivityContext) : ViewScaleProvider { + + override fun getScaleFromItemInfo(info: ItemInfo?): PointF { + if (info !is LauncherAppWidgetInfo) { + return DEFAULT_SCALE + } + + val density = target.asContext().resources.displayMetrics.density + if (density == 0f) return DEFAULT_SCALE + + val existingSizes: List + try { + existingSizes = + AppWidgetManager.getInstance(target.asContext()) + .getAppWidgetOptions(info.appWidgetId) + .getWidgetSizeList() ?: return DEFAULT_SCALE + } catch (e: Exception) { + // Failed to get widget options, ignore + return DEFAULT_SCALE + } + + val expectedSize = + WidgetSizes.getWidgetSizePx(target.deviceProfile, info.spanX, info.spanY).let { + SizeF(it.width / density, it.height / density) + } + if (expectedSize.height <= 0 || expectedSize.width <= 0) return DEFAULT_SCALE + + // Find the size which is closest to the expected size + val bestOriginalSize = + existingSizes + .asSequence() + .filter { it.height > 0 && it.width > 0 } + .minByOrNull { + (it.width - expectedSize.width).pow(2) + + (it.height - expectedSize.height).pow(2) + } ?: return DEFAULT_SCALE + + return PointF( + expectedSize.width / bestOriginalSize.width, + expectedSize.height / bestOriginalSize.height, + ) + } +} diff --git a/src/com/android/launcher3/widget/util/WidgetSizeHandler.kt b/src/com/android/launcher3/widget/util/WidgetSizeHandler.kt new file mode 100644 index 0000000000..6a2bb9efa0 --- /dev/null +++ b/src/com/android/launcher3/widget/util/WidgetSizeHandler.kt @@ -0,0 +1,63 @@ +/* + * 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.widget.util + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_SIZES +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.os.Bundle +import android.util.SizeF +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.util.Executors +import javax.inject.Inject + +/** Helper class for handling widget updates */ +open class WidgetSizeHandler @Inject constructor(@ApplicationContext private val context: Context) { + + /** + * Updates the widget size range if it is not currently the same. This makes two binder calls, + * one for getting the existing options, [AppWidgetManager.getAppWidgetOptions] and if it + * doesn't match the expected value, another call to update it, + * [AppWidgetManager.updateAppWidgetOptions]. + * + * Note that updating the options is a costly call as it wakes up the provider process and + * causes a full widget update, hence two binder calls are preferable over unnecessarily + * updating the widget options. + */ + open fun updateSizeRangesAsync( + widgetId: Int, + info: AppWidgetProviderInfo, + spanX: Int, + spanY: Int, + ) { + Executors.UI_HELPER_EXECUTOR.execute { + val widgetManager = AppWidgetManager.getInstance(context) + val sizeOptions = WidgetSizes.getWidgetSizeOptions(context, info.provider, spanX, spanY) + if ( + sizeOptions.getWidgetSizeList() != + widgetManager.getAppWidgetOptions(widgetId).getWidgetSizeList() + ) + widgetManager.updateAppWidgetOptions(widgetId, sizeOptions) + } + } + + companion object { + + fun Bundle.getWidgetSizeList() = getParcelableArrayList(OPTION_APPWIDGET_SIZES) + } +} diff --git a/src/com/android/launcher3/widget/util/WidgetSizes.java b/src/com/android/launcher3/widget/util/WidgetSizes.java index 4688359a0c..66c31e139e 100644 --- a/src/com/android/launcher3/widget/util/WidgetSizes.java +++ b/src/com/android/launcher3/widget/util/WidgetSizes.java @@ -15,8 +15,6 @@ */ package com.android.launcher3.widget.util; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; - import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; @@ -25,13 +23,13 @@ import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; -import android.util.Log; import android.util.Size; import android.util.SizeF; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.dagger.LauncherComponentProvider; import com.android.launcher3.model.WidgetItem; import java.util.ArrayList; @@ -110,17 +108,8 @@ public final class WidgetSizes { return; } - UI_HELPER_EXECUTOR.execute(() -> { - AppWidgetManager widgetManager = AppWidgetManager.getInstance(context); - Bundle sizeOptions = getWidgetSizeOptions(context, info.provider, spanX, spanY); - if (sizeOptions.getParcelableArrayList( - AppWidgetManager.OPTION_APPWIDGET_SIZES).equals( - widgetManager.getAppWidgetOptions(widgetId).getParcelableArrayList( - AppWidgetManager.OPTION_APPWIDGET_SIZES))) { - return; - } - widgetManager.updateAppWidgetOptions(widgetId, sizeOptions); - }); + LauncherComponentProvider.get(context).getWidgetSizeHandler() + .updateSizeRangesAsync(widgetId, info, spanX, spanY); } /** @@ -137,8 +126,6 @@ public final class WidgetSizes { options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, rect.right); options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, rect.bottom); options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes); - Log.d("b/267448330", "provider: " + provider + ", paddedSizes: " + paddedSizes - + ", getMinMaxSizes: " + rect); return options; }