Fixing widget size incorreectly updated during preview

Bug: 408934352
Bug: 228328759
Flag: EXEMPT bugfix
Test: Manually verified
Change-Id: Ie69e67cfaee6231da3971323ae98d2b3ab514c0d
This commit is contained in:
Sunny Goyal
2025-06-01 10:58:42 -07:00
parent d3c7533a55
commit f6b98ada0d
7 changed files with 172 additions and 32 deletions
@@ -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. */
@@ -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
@@ -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);
@@ -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
}
}
@@ -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<SizeF>
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,
)
}
}
@@ -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<SizeF>(OPTION_APPWIDGET_SIZES)
}
}
@@ -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.<SizeF>getParcelableArrayList(
AppWidgetManager.OPTION_APPWIDGET_SIZES).equals(
widgetManager.getAppWidgetOptions(widgetId).<SizeF>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;
}