diff options
| author | 2021-02-17 00:34:18 +0000 | |
|---|---|---|
| committer | 2021-02-17 00:34:18 +0000 | |
| commit | 574718564e58a1c5bd5abcaa776ea6adb666de2f (patch) | |
| tree | efcd440226b68c05f672bb109c3b86458a82320d | |
| parent | 6c9f4850fb8fe6f8ff6777258f3e992b04b53b18 (diff) | |
| parent | d7541b8990a1a7012dc78d82f0620122d57fcf00 (diff) | |
Merge "Framework changes for go/widget-size-specification" into sc-dev
| -rw-r--r-- | core/api/current.txt | 7 | ||||
| -rw-r--r-- | core/java/android/appwidget/AppWidgetHostView.java | 105 | ||||
| -rw-r--r-- | core/java/android/appwidget/AppWidgetManager.java | 6 | ||||
| -rw-r--r-- | core/java/android/widget/RemoteViews.java | 341 |
4 files changed, 419 insertions, 40 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 08e63660c3d9..9cbf12f7d511 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8309,6 +8309,7 @@ package android.appwidget { public class AppWidgetHostView extends android.widget.FrameLayout { ctor public AppWidgetHostView(android.content.Context); ctor public AppWidgetHostView(android.content.Context, int, int); + method public void clearCurrentSize(); method public int getAppWidgetId(); method public android.appwidget.AppWidgetProviderInfo getAppWidgetInfo(); method public static android.graphics.Rect getDefaultPaddingForWidget(android.content.Context, android.content.ComponentName, android.graphics.Rect); @@ -8316,11 +8317,13 @@ package android.appwidget { method protected android.view.View getErrorView(); method protected void prepareView(android.view.View); method public void setAppWidget(int, android.appwidget.AppWidgetProviderInfo); + method public void setCurrentSize(@NonNull android.graphics.PointF); method public void setExecutor(java.util.concurrent.Executor); method public void setOnLightBackground(boolean); method public void updateAppWidget(android.widget.RemoteViews); method public void updateAppWidgetOptions(android.os.Bundle); - method public void updateAppWidgetSize(android.os.Bundle, int, int, int, int); + method @Deprecated public void updateAppWidgetSize(android.os.Bundle, int, int, int, int); + method public void updateAppWidgetSize(@NonNull android.os.Bundle, @NonNull java.util.List<android.graphics.PointF>); } public class AppWidgetManager { @@ -8373,6 +8376,7 @@ package android.appwidget { field public static final String OPTION_APPWIDGET_MIN_HEIGHT = "appWidgetMinHeight"; field public static final String OPTION_APPWIDGET_MIN_WIDTH = "appWidgetMinWidth"; field public static final String OPTION_APPWIDGET_RESTORE_COMPLETED = "appWidgetRestoreCompleted"; + field public static final String OPTION_APPWIDGET_SIZES = "appWidgetSizes"; } public class AppWidgetProvider extends android.content.BroadcastReceiver { @@ -54649,6 +54653,7 @@ package android.widget { public class RemoteViews implements android.view.LayoutInflater.Filter android.os.Parcelable { ctor public RemoteViews(String, int); ctor public RemoteViews(android.widget.RemoteViews, android.widget.RemoteViews); + ctor public RemoteViews(@NonNull java.util.Map<android.graphics.PointF,android.widget.RemoteViews>); ctor public RemoteViews(android.widget.RemoteViews); ctor public RemoteViews(android.os.Parcel); method public void addView(@IdRes int, android.widget.RemoteViews); diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index a3c3a0e106a3..42d90a794e74 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -16,6 +16,7 @@ package android.appwidget; +import android.annotation.NonNull; import android.app.Activity; import android.app.ActivityOptions; import android.compat.annotation.UnsupportedAppUsage; @@ -29,6 +30,7 @@ import android.content.pm.LauncherApps; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.PointF; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; @@ -53,6 +55,7 @@ import android.widget.TextView; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; /** @@ -89,6 +92,7 @@ public class AppWidgetHostView extends FrameLayout { int mLayoutId = -1; private OnClickHandler mOnClickHandler; private boolean mOnLightBackground; + PointF mCurrentSize = null; private Executor mAsyncExecutor; private CancellationSignal mLastExecutionSignal; @@ -268,7 +272,8 @@ public class AppWidgetHostView extends FrameLayout { * Provide guidance about the size of this widget to the AppWidgetManager. The widths and * heights should correspond to the full area the AppWidgetHostView is given. Padding added by * the framework will be accounted for automatically. This information gets embedded into the - * AppWidget options and causes a callback to the AppWidgetProvider. + * AppWidget options and causes a callback to the AppWidgetProvider. In addition, the list of + * sizes is explicitly set to an empty list. * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) * * @param newOptions The bundle of options, in addition to the size information, @@ -277,14 +282,97 @@ public class AppWidgetHostView extends FrameLayout { * @param minHeight The maximum height in dips that the widget will be displayed at. * @param maxWidth The maximum width in dips that the widget will be displayed at. * @param maxHeight The maximum height in dips that the widget will be displayed at. - * + * @deprecated use {@link AppWidgetHostView#updateAppWidgetSize(Bundle, List)} instead. */ + @Deprecated public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight) { updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false); } /** + * Provide guidance about the size of this widget to the AppWidgetManager. The sizes should + * correspond to the full area the AppWidgetHostView is given. Padding added by the framework + * will be accounted for automatically. + * + * This method will update the option bundle with the list of sizes and the min/max bounds for + * width and height. + * + * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) + * + * @param newOptions The bundle of options, in addition to the size information. + * @param sizes Sizes, in dips, the widget may be displayed at without calling the provider + * again. Typically, this will be size of the widget in landscape and portrait. + * On some foldables, this might include the size on the outer and inner screens. + */ + public void updateAppWidgetSize(@NonNull Bundle newOptions, @NonNull List<PointF> sizes) { + AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); + + Rect padding = getDefaultPadding(); + float density = getResources().getDisplayMetrics().density; + + float xPaddingDips = (padding.left + padding.right) / density; + float yPaddingDips = (padding.top + padding.bottom) / density; + + ArrayList<PointF> paddedSizes = new ArrayList<>(sizes.size()); + float minWidth = Float.MAX_VALUE; + float maxWidth = 0; + float minHeight = Float.MAX_VALUE; + float maxHeight = 0; + for (int i = 0; i < sizes.size(); i++) { + PointF size = sizes.get(i); + PointF paddedPoint = new PointF(Math.max(0.f, size.x - xPaddingDips), + Math.max(0.f, size.y - yPaddingDips)); + paddedSizes.add(paddedPoint); + minWidth = Math.min(minWidth, paddedPoint.x); + maxWidth = Math.max(maxWidth, paddedPoint.x); + minHeight = Math.min(minHeight, paddedPoint.y); + maxHeight = Math.max(maxHeight, paddedPoint.y); + } + if (paddedSizes.equals( + widgetManager.getAppWidgetOptions(mAppWidgetId).<PointF>getParcelableArrayList( + AppWidgetManager.OPTION_APPWIDGET_SIZES))) { + return; + } + Bundle options = newOptions.deepCopy(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, (int) minWidth); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, (int) minHeight); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, (int) maxWidth); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, (int) maxHeight); + options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes); + updateAppWidgetOptions(options); + } + + /** + * Set the current size of the widget. This should be the full area the AppWidgetHostView is + * given. Padding added by the framework will be accounted for automatically. + * + * This size will be used to choose the appropriate layout the next time the {@link RemoteViews} + * is re-inflated, if it was created with {@link RemoteViews#RemoteViews(Map)} . + */ + public void setCurrentSize(@NonNull PointF size) { + Rect padding = getDefaultPadding(); + float density = getResources().getDisplayMetrics().density; + float xPaddingDips = (padding.left + padding.right) / density; + float yPaddingDips = (padding.top + padding.bottom) / density; + PointF newSize = new PointF(size.x - xPaddingDips, size.y - yPaddingDips); + if (!newSize.equals(mCurrentSize)) { + mCurrentSize = newSize; + mLayoutId = -1; // Prevents recycling the view. + } + } + + /** + * Clear the current size, indicating it is not currently known. + */ + public void clearCurrentSize() { + if (mCurrentSize != null) { + mCurrentSize = null; + mLayoutId = -1; + } + } + + /** * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -322,6 +410,8 @@ public class AppWidgetHostView extends FrameLayout { newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight); newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth); newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight); + newOptions.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, + new ArrayList<PointF>()); updateAppWidgetOptions(newOptions); } } @@ -440,7 +530,7 @@ public class AppWidgetHostView extends FrameLayout { // Try normal RemoteView inflation if (content == null) { try { - content = remoteViews.apply(mContext, this, mOnClickHandler); + content = remoteViews.apply(mContext, this, mOnClickHandler, mCurrentSize); if (LOGD) Log.d(TAG, "had to inflate new layout"); } catch (RuntimeException e) { exception = e; @@ -492,7 +582,8 @@ public class AppWidgetHostView extends FrameLayout { mView, mAsyncExecutor, new ViewApplyListener(remoteViews, layoutId, true), - mOnClickHandler); + mOnClickHandler, + mCurrentSize); } catch (Exception e) { // Reapply failed. Try apply } @@ -502,7 +593,8 @@ public class AppWidgetHostView extends FrameLayout { this, mAsyncExecutor, new ViewApplyListener(remoteViews, layoutId, false), - mOnClickHandler); + mOnClickHandler, + mCurrentSize); } } @@ -533,7 +625,8 @@ public class AppWidgetHostView extends FrameLayout { AppWidgetHostView.this, mAsyncExecutor, new ViewApplyListener(mViews, mLayoutId, false), - mOnClickHandler); + mOnClickHandler, + mCurrentSize); } else { applyContent(null, false, e); } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 37093a10b2f0..aac8710e8691 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -217,6 +217,12 @@ public class AppWidgetManager { public static final String OPTION_APPWIDGET_MAX_HEIGHT = "appWidgetMaxHeight"; /** + * A bundle extra ({@code List<PointF>}) that contains the list of possible sizes, in dips, a + * widget instance can take. + */ + public static final String OPTION_APPWIDGET_SIZES = "appWidgetSizes"; + + /** * A bundle extra that hints to the AppWidgetProvider the category of host that owns this * this widget. Can have the value {@link * AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN} or {@link diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 6cb4b81827b9..4e3d99b43cf6 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -48,6 +48,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Outline; +import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -99,6 +100,8 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Stack; @@ -171,6 +174,11 @@ public class RemoteViews implements Parcelable, Filter { */ private static final int MAX_NESTED_VIEWS = 10; + /** + * Maximum number of RemoteViews that can be specified in constructor. + */ + private static final int MAX_INIT_VIEW_COUNT = 16; + // The unique identifiers for each custom {@link Action}. private static final int SET_ON_CLICK_RESPONSE_TAG = 1; private static final int REFLECTION_ACTION_TAG = 2; @@ -290,7 +298,7 @@ public class RemoteViews implements Parcelable, Filter { * The resource ID of the layout file. (Added to the parcel) */ @UnsupportedAppUsage - private final int mLayoutId; + private int mLayoutId; /** * The resource ID of the layout file in dark text mode. (Added to the parcel) @@ -322,6 +330,7 @@ public class RemoteViews implements Parcelable, Filter { */ private static final int MODE_NORMAL = 0; private static final int MODE_HAS_LANDSCAPE_AND_PORTRAIT = 1; + private static final int MODE_HAS_SIZED_REMOTEVIEWS = 2; /** * Used in conjunction with the special constructor @@ -331,12 +340,26 @@ public class RemoteViews implements Parcelable, Filter { private RemoteViews mLandscape = null; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private RemoteViews mPortrait = null; + /** + * List of RemoteViews with their ideal size. There must be at least two if the map is not null. + * + * The smallest remote view is always the last element in the list. + */ + private List<RemoteViews> mSizedRemoteViews = null; + + /** + * Ideal size for this RemoteViews. + * + * Only to be used on children views used in a {@link RemoteViews} with + * {@link RemoteViews#hasSizedRemoteViews()}. + */ + private PointF mIdealSize = null; @ApplyFlags private int mApplyFlags = 0; /** Class cookies of the Parcel this instance was read from. */ - private final Map<Class, Object> mClassCookies; + private Map<Class, Object> mClassCookies; private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = (view, pendingIntent, response) -> startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)); @@ -2768,23 +2791,50 @@ public class RemoteViews implements Parcelable, Filter { mClassCookies = null; } + private boolean hasMultipleLayouts() { + return hasLandscapeAndPortraitLayouts() || hasSizedRemoteViews(); + } + private boolean hasLandscapeAndPortraitLayouts() { return (mLandscape != null) && (mPortrait != null); } + private boolean hasSizedRemoteViews() { + return mSizedRemoteViews != null; + } + + private @Nullable PointF getIdealSize() { + return mIdealSize; + } + + private void setIdealSize(@Nullable PointF size) { + mIdealSize = size; + } + + /** + * Finds the smallest view in {@code mSizedRemoteViews}. + * This method must not be called if {@code mSizedRemoteViews} is null. + */ + private RemoteViews findSmallestRemoteView() { + return mSizedRemoteViews.get(mSizedRemoteViews.size() - 1); + } + /** * Create a new RemoteViews object that will inflate as the specified * landspace or portrait RemoteViews, depending on the current configuration. * * @param landscape The RemoteViews to inflate in landscape configuration * @param portrait The RemoteViews to inflate in portrait configuration + * @throws IllegalArgumentException if either landscape or portrait are null or if they are + * not from the same application */ public RemoteViews(RemoteViews landscape, RemoteViews portrait) { if (landscape == null || portrait == null) { - throw new RuntimeException("Both RemoteViews must be non-null"); + throw new IllegalArgumentException("Both RemoteViews must be non-null"); } if (!landscape.hasSameAppInfo(portrait.mApplication)) { - throw new RuntimeException("Both RemoteViews must share the same package and user"); + throw new IllegalArgumentException( + "Both RemoteViews must share the same package and user"); } mApplication = portrait.mApplication; mLayoutId = portrait.mLayoutId; @@ -2802,9 +2852,84 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Create a new RemoteViews object that will inflate the layout with the closest size + * specification. + * + * The default remote views in that case is always the smallest one provided. + * + * @param remoteViews Mapping of size to layout. + * @throws IllegalArgumentException if the map is empty, there are more than + * MAX_INIT_VIEW_COUNT layouts or the remote views are not all from the same application. + */ + public RemoteViews(@NonNull Map<PointF, RemoteViews> remoteViews) { + if (remoteViews.isEmpty()) { + throw new IllegalArgumentException("The set of RemoteViews cannot be empty"); + } + if (remoteViews.size() > MAX_INIT_VIEW_COUNT) { + throw new IllegalArgumentException("Too many RemoteViews in constructor"); + } + if (remoteViews.size() == 1) { + initializeFrom(remoteViews.values().iterator().next()); + return; + } + mBitmapCache = new BitmapCache(); + mClassCookies = initializeSizedRemoteViews( + remoteViews.entrySet().stream().map( + entry -> { + entry.getValue().setIdealSize(entry.getKey()); + return entry.getValue(); + } + ).iterator() + ); + + RemoteViews smallestView = findSmallestRemoteView(); + mApplication = smallestView.mApplication; + mLayoutId = smallestView.mLayoutId; + mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; + } + + // Initialize mSizedRemoteViews and return the class cookies. + private Map<Class, Object> initializeSizedRemoteViews(Iterator<RemoteViews> remoteViews) { + List<RemoteViews> sizedRemoteViews = new ArrayList<>(); + Map<Class, Object> classCookies = null; + float viewArea = Float.MAX_VALUE; + RemoteViews smallestView = null; + while (remoteViews.hasNext()) { + RemoteViews view = remoteViews.next(); + PointF size = view.getIdealSize(); + float newViewArea = size.x * size.y; + if (smallestView != null && !view.hasSameAppInfo(smallestView.mApplication)) { + throw new IllegalArgumentException( + "All RemoteViews must share the same package and user"); + } + if (smallestView == null || newViewArea < viewArea) { + if (smallestView != null) { + sizedRemoteViews.add(smallestView); + } + viewArea = newViewArea; + smallestView = view; + } else { + sizedRemoteViews.add(view); + } + configureRemoteViewsAsChild(view); + view.setIdealSize(size); + if (classCookies == null) { + classCookies = view.mClassCookies; + } + } + sizedRemoteViews.add(smallestView); + mSizedRemoteViews = sizedRemoteViews; + return classCookies; + } + + /** * Creates a copy of another RemoteViews. */ public RemoteViews(RemoteViews src) { + initializeFrom(src); + } + + private void initializeFrom(RemoteViews src) { mBitmapCache = src.mBitmapCache; mApplication = src.mApplication; mIsRoot = src.mIsRoot; @@ -2812,12 +2937,20 @@ public class RemoteViews implements Parcelable, Filter { mLightBackgroundLayoutId = src.mLightBackgroundLayoutId; mApplyFlags = src.mApplyFlags; mClassCookies = src.mClassCookies; + mIdealSize = src.mIdealSize; if (src.hasLandscapeAndPortraitLayouts()) { mLandscape = new RemoteViews(src.mLandscape); mPortrait = new RemoteViews(src.mPortrait); } + if (src.hasSizedRemoteViews()) { + mSizedRemoteViews = new ArrayList<>(src.mSizedRemoteViews.size()); + for (RemoteViews srcView : src.mSizedRemoteViews) { + mSizedRemoteViews.add(new RemoteViews(srcView)); + } + } + if (src.mActions != null) { Parcel p = Parcel.obtain(); p.putClassCookies(mClassCookies); @@ -2867,10 +3000,29 @@ public class RemoteViews implements Parcelable, Filter { if (mode == MODE_NORMAL) { mApplication = parcel.readInt() == 0 ? info : ApplicationInfo.CREATOR.createFromParcel(parcel); + mIdealSize = parcel.readInt() == 0 ? null : PointF.CREATOR.createFromParcel(parcel); mLayoutId = parcel.readInt(); mLightBackgroundLayoutId = parcel.readInt(); readActionsFromParcel(parcel, depth); + } else if (mode == MODE_HAS_SIZED_REMOTEVIEWS) { + int numViews = parcel.readInt(); + if (numViews > MAX_INIT_VIEW_COUNT) { + throw new IllegalArgumentException( + "Too many views in mapping from size to RemoteViews."); + } + List<RemoteViews> remoteViews = new ArrayList<>(numViews); + for (int i = 0; i < numViews; i++) { + RemoteViews view = new RemoteViews(parcel, mBitmapCache, info, depth, + mClassCookies); + info = view.mApplication; + remoteViews.add(view); + } + initializeSizedRemoteViews(remoteViews.iterator()); + RemoteViews smallestView = findSmallestRemoteView(); + mApplication = smallestView.mApplication; + mLayoutId = smallestView.mLayoutId; + mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; } else { // MODE_HAS_LANDSCAPE_AND_PORTRAIT mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth, mClassCookies); @@ -2990,16 +3142,20 @@ public class RemoteViews implements Parcelable, Filter { */ private void setBitmapCache(BitmapCache bitmapCache) { mBitmapCache = bitmapCache; - if (!hasLandscapeAndPortraitLayouts()) { + if (hasSizedRemoteViews()) { + for (RemoteViews remoteView : mSizedRemoteViews) { + remoteView.setBitmapCache(bitmapCache); + } + } else if (hasLandscapeAndPortraitLayouts()) { + mLandscape.setBitmapCache(bitmapCache); + mPortrait.setBitmapCache(bitmapCache); + } else { if (mActions != null) { final int count = mActions.size(); - for (int i= 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { mActions.get(i).setBitmapCache(bitmapCache); } } - } else { - mLandscape.setBitmapCache(bitmapCache); - mPortrait.setBitmapCache(bitmapCache); } } @@ -3018,10 +3174,10 @@ public class RemoteViews implements Parcelable, Filter { * @param a The action to add */ private void addAction(Action a) { - if (hasLandscapeAndPortraitLayouts()) { - throw new RuntimeException("RemoteViews specifying separate landscape and portrait" + - " layouts cannot be modified. Instead, fully configure the landscape and" + - " portrait layouts individually before constructing the combined layout."); + if (hasMultipleLayouts()) { + throw new RuntimeException("RemoteViews specifying separate layouts for orientation" + + " or size cannot be modified. Instead, fully configure each layouts" + + " individually before constructing the combined layout."); } if (mActions == null) { mActions = new ArrayList<>(); @@ -4100,14 +4256,79 @@ public class RemoteViews implements Parcelable, Filter { int orientation = context.getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { return mLandscape; - } else { - return mPortrait; } + return mPortrait; + } + if (hasSizedRemoteViews()) { + return findSmallestRemoteView(); } return this; } /** + * Returns the square distance between two points. + * + * This is particularly useful when we only care about the ordering of the distances. + */ + private static float squareDistance(PointF p1, PointF p2) { + float dx = p1.x - p2.x; + float dy = p1.y - p2.y; + return dx * dx + dy * dy; + } + + /** + * Returns whether the layout fits in the space available to the widget. + * + * A layout fits on a widget if the widget size is known (i.e. not null) and both dimensions + * are smaller than the ones of the widget, adding some padding to account for rounding errors. + */ + private static boolean fitsIn(PointF sizeLayout, @Nullable PointF sizeWidget) { + return sizeWidget != null && (Math.ceil(sizeWidget.x) + 1 > sizeLayout.x) + && (Math.ceil(sizeWidget.y) + 1 > sizeLayout.y); + } + + /** + * Returns the most appropriate {@link RemoteViews} given the context and, if not null, the + * size of the widget. + * + * If {@link RemoteViews#hasSizedRemoteViews()} returns true, the most appropriate view is + * the one that fits in the widget (according to {@link RemoteViews#fitsIn}) and has the + * diagonal the most similar to the widget. If no layout fits or the size of the widget is + * not specified, the one with the smallest area will be chosen. + */ + private RemoteViews getRemoteViewsToApply(@NonNull Context context, + @Nullable PointF widgetSize) { + if (!hasSizedRemoteViews()) { + // If there isn't multiple remote views, fall back on the previous methods. + return getRemoteViewsToApply(context); + } + // Find the better remote view + RemoteViews bestFit = null; + float bestSqDist = Float.MAX_VALUE; + for (RemoteViews layout : mSizedRemoteViews) { + PointF layoutSize = layout.getIdealSize(); + if (fitsIn(layoutSize, widgetSize)) { + if (bestFit == null) { + bestFit = layout; + bestSqDist = squareDistance(layoutSize, widgetSize); + } else { + float newSqDist = squareDistance(layoutSize, widgetSize); + if (newSqDist < bestSqDist) { + bestFit = layout; + bestSqDist = newSqDist; + } + } + } + } + if (bestFit == null) { + Log.w(LOG_TAG, "Could not find a RemoteViews fitting the current size: " + widgetSize); + return findSmallestRemoteView(); + } + return bestFit; + } + + + /** * Inflates the view hierarchy represented by this object and applies * all of the actions. * @@ -4124,7 +4345,13 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public View apply(Context context, ViewGroup parent, OnClickHandler handler) { - RemoteViews rvToApply = getRemoteViewsToApply(context); + return apply(context, parent, handler, null); + } + + /** @hide */ + public View apply(@NonNull Context context, @NonNull ViewGroup parent, + @Nullable OnClickHandler handler, @Nullable PointF size) { + RemoteViews rvToApply = getRemoteViewsToApply(context, size); View result = inflateView(context, rvToApply, parent); rvToApply.performApply(result, parent, handler); @@ -4132,9 +4359,17 @@ public class RemoteViews implements Parcelable, Filter { } /** @hide */ - public View applyWithTheme(Context context, ViewGroup parent, OnClickHandler handler, + public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent, + @Nullable OnClickHandler handler, @StyleRes int applyThemeResId) { - RemoteViews rvToApply = getRemoteViewsToApply(context); + return applyWithTheme(context, parent, handler, applyThemeResId, null); + } + + /** @hide */ + public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent, + @Nullable OnClickHandler handler, + @StyleRes int applyThemeResId, @Nullable PointF size) { + RemoteViews rvToApply = getRemoteViewsToApply(context, size); View result = inflateView(context, rvToApply, parent, applyThemeResId); rvToApply.performApply(result, parent, handler); @@ -4219,12 +4454,26 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, OnClickHandler handler) { - return getAsyncApplyTask(context, parent, listener, handler).startTaskOnExecutor(executor); + return applyAsync(context, parent, executor, listener, handler, null); + } + + /** @hide */ + public CancellationSignal applyAsync(Context context, ViewGroup parent, + Executor executor, OnViewAppliedListener listener, OnClickHandler handler, + PointF size) { + return getAsyncApplyTask(context, parent, listener, handler, size).startTaskOnExecutor( + executor); } private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent, OnViewAppliedListener listener, OnClickHandler handler) { - return new AsyncApplyTask(getRemoteViewsToApply(context), parent, context, listener, + return getAsyncApplyTask(context, parent, listener, handler, null); + } + + private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent, + OnViewAppliedListener listener, OnClickHandler handler, PointF size) { + return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, + listener, handler, null); } @@ -4341,12 +4590,18 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public void reapply(Context context, View v, OnClickHandler handler) { - RemoteViews rvToApply = getRemoteViewsToApply(context); + reapply(context, v, handler, null); + } - // In the case that a view has this RemoteViews applied in one orientation, is persisted - // across orientation change, and has the RemoteViews re-applied in the new orientation, - // we throw an exception, since the layouts may be completely unrelated. - if (hasLandscapeAndPortraitLayouts()) { + /** @hide */ + public void reapply(Context context, View v, OnClickHandler handler, PointF size) { + RemoteViews rvToApply = getRemoteViewsToApply(context, size); + + // In the case that a view has this RemoteViews applied in one orientation or size, is + // persisted across change, and has the RemoteViews re-applied in a different situation + // (orientation or size), we throw an exception, since the layouts may be completely + // unrelated. + if (hasMultipleLayouts()) { if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) { throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" + " that does not share the same root layout id."); @@ -4377,12 +4632,18 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public CancellationSignal reapplyAsync(Context context, View v, Executor executor, OnViewAppliedListener listener, OnClickHandler handler) { - RemoteViews rvToApply = getRemoteViewsToApply(context); + return reapplyAsync(context, v, executor, listener, handler, null); + } + + /** @hide */ + public CancellationSignal reapplyAsync(Context context, View v, Executor executor, + OnViewAppliedListener listener, OnClickHandler handler, PointF size) { + RemoteViews rvToApply = getRemoteViewsToApply(context, size); // In the case that a view has this RemoteViews applied in one orientation, is persisted // across orientation change, and has the RemoteViews re-applied in the new orientation, // we throw an exception, since the layouts may be completely unrelated. - if (hasLandscapeAndPortraitLayouts()) { + if (hasMultipleLayouts()) { if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) { throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" + " that does not share the same root layout id."); @@ -4466,7 +4727,7 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - if (!hasLandscapeAndPortraitLayouts()) { + if (!hasMultipleLayouts()) { dest.writeInt(MODE_NORMAL); // We only write the bitmap cache if we are the root RemoteViews, as this cache // is shared by all children. @@ -4479,9 +4740,26 @@ public class RemoteViews implements Parcelable, Filter { dest.writeInt(1); mApplication.writeToParcel(dest, flags); } + if (mIsRoot || mIdealSize == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + mIdealSize.writeToParcel(dest, flags); + } dest.writeInt(mLayoutId); dest.writeInt(mLightBackgroundLayoutId); writeActionsToParcel(dest); + } else if (hasSizedRemoteViews()) { + dest.writeInt(MODE_HAS_SIZED_REMOTEVIEWS); + if (mIsRoot) { + mBitmapCache.writeBitmapsToParcel(dest, flags); + } + int childFlags = flags; + dest.writeInt(mSizedRemoteViews.size()); + for (RemoteViews view : mSizedRemoteViews) { + view.writeToParcel(dest, childFlags); + childFlags |= PARCELABLE_ELIDE_DUPLICATES; + } } else { dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT); // We only write the bitmap cache if we are the root RemoteViews, as this cache @@ -4735,11 +5013,9 @@ public class RemoteViews implements Parcelable, Filter { * before starting the intent. * * @param fillIntent The intent which will be combined with the parent's PendingIntent in - * order to determine the behavior of the response - * + * order to determine the behavior of the response * @see RemoteViews#setPendingIntentTemplate(int, PendingIntent) * @see RemoteViews#setOnClickFillInIntent(int, Intent) - * @return */ @NonNull public static RemoteResponse fromFillInIntent(@NonNull Intent fillIntent) { @@ -4754,9 +5030,8 @@ public class RemoteViews implements Parcelable, Filter { * the epicenter for the exit Transition. The position of the associated shared element in * the launched Activity will be the epicenter of its entering Transition. * - * @param viewId The id of the view to be shared as part of the transition + * @param viewId The id of the view to be shared as part of the transition * @param sharedElementName The shared element name for this view - * * @see ActivityOptions#makeSceneTransitionAnimation(Activity, Pair[]) */ @NonNull |