/* * Copyright (C) 2007 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 android.widget; import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL; import static android.appwidget.flags.Flags.drawDataParcel; import static android.appwidget.flags.Flags.remoteAdapterConversion; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; import android.annotation.AttrRes; import android.annotation.ColorInt; import android.annotation.ColorRes; import android.annotation.DimenRes; import android.annotation.DrawableRes; import android.annotation.FlaggedApi; import android.annotation.IdRes; import android.annotation.IntDef; import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; import android.annotation.StringRes; import android.annotation.StyleRes; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; import android.app.LoadedApk; import android.app.PendingIntent; import android.app.RemoteInput; import android.appwidget.AppWidgetHostView; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.loader.ResourcesLoader; import android.content.res.loader.ResourcesProvider; import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Outline; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; import android.os.StrictMode; import android.os.UserHandle; import android.system.Os; import android.text.TextUtils; import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.IntArray; import android.util.Log; import android.util.LongArray; import android.util.Pair; import android.util.SizeF; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; import android.util.TypedValue.ComplexDimensionUnit; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; import android.view.MotionEvent; import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewManager; import android.view.ViewOutlineProvider; import android.view.ViewParent; import android.view.ViewStub; import android.widget.AdapterView.OnItemClickListener; import android.widget.CompoundButton.OnCheckedChangeListener; import com.android.internal.R; import com.android.internal.util.Preconditions; import com.android.internal.widget.IRemoteViewsFactory; import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.player.RemoteComposeDocument; import com.android.internal.widget.remotecompose.player.RemoteComposePlayer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; /** * A class that describes a view hierarchy that can be displayed in * another process. The hierarchy is inflated from a layout resource * file, and this class provides some basic operations for modifying * the content of the inflated hierarchy. * *

{@code RemoteViews} is limited to support for the following layouts:

* *

And the following widgets:

* *

As of API 31, the following widgets and layouts may also be used:

* *

Descendants of these classes are not supported.

*/ public class RemoteViews implements Parcelable, Filter { private static final String LOG_TAG = "RemoteViews"; /** The intent extra for whether the view whose checked state changed is currently checked. */ public static final String EXTRA_CHECKED = "android.widget.extra.CHECKED"; /** * The intent extra that contains the appWidgetId. * @hide */ static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId"; /** * The intent extra that contains {@code true} if inflating as dak text theme. * @hide */ static final String EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND = "remoteAdapterOnLightBackground"; /** * The intent extra that contains the bounds for all shared elements. */ public static final String EXTRA_SHARED_ELEMENT_BOUNDS = "android.widget.extra.SHARED_ELEMENT_BOUNDS"; /** * Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and * {@link #RemoteViews(RemoteViews, RemoteViews)}. */ 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; private static final int SET_DRAWABLE_TINT_TAG = 3; private static final int VIEW_GROUP_ACTION_ADD_TAG = 4; private static final int VIEW_CONTENT_NAVIGATION_TAG = 5; private static final int SET_EMPTY_VIEW_ACTION_TAG = 6; private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7; private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8; private static final int SET_REMOTE_VIEW_ADAPTER_INTENT_TAG = 10; private static final int TEXT_VIEW_DRAWABLE_ACTION_TAG = 11; private static final int BITMAP_REFLECTION_ACTION_TAG = 12; private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13; private static final int VIEW_PADDING_ACTION_TAG = 14; private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18; private static final int LAYOUT_PARAM_ACTION_TAG = 19; private static final int SET_RIPPLE_DRAWABLE_COLOR_TAG = 21; private static final int SET_INT_TAG_TAG = 22; private static final int REMOVE_FROM_PARENT_ACTION_TAG = 23; private static final int RESOURCE_REFLECTION_ACTION_TAG = 24; private static final int COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG = 25; private static final int SET_COMPOUND_BUTTON_CHECKED_TAG = 26; private static final int SET_RADIO_GROUP_CHECKED = 27; private static final int SET_VIEW_OUTLINE_RADIUS_TAG = 28; private static final int SET_ON_CHECKED_CHANGE_RESPONSE_TAG = 29; private static final int NIGHT_MODE_REFLECTION_ACTION_TAG = 30; private static final int SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG = 31; private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32; private static final int SET_REMOTE_ADAPTER_TAG = 33; private static final int SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG = 34; private static final int SET_DRAW_INSTRUCTION_TAG = 35; /** @hide **/ @IntDef(prefix = "MARGIN_", value = { MARGIN_LEFT, MARGIN_TOP, MARGIN_RIGHT, MARGIN_BOTTOM, MARGIN_START, MARGIN_END }) @Retention(RetentionPolicy.SOURCE) public @interface MarginType {} /** The value will apply to the marginLeft. */ public static final int MARGIN_LEFT = 0; /** The value will apply to the marginTop. */ public static final int MARGIN_TOP = 1; /** The value will apply to the marginRight. */ public static final int MARGIN_RIGHT = 2; /** The value will apply to the marginBottom. */ public static final int MARGIN_BOTTOM = 3; /** The value will apply to the marginStart. */ public static final int MARGIN_START = 4; /** The value will apply to the marginEnd. */ public static final int MARGIN_END = 5; @IntDef(prefix = "VALUE_TYPE_", value = { VALUE_TYPE_RAW, VALUE_TYPE_COMPLEX_UNIT, VALUE_TYPE_RESOURCE, VALUE_TYPE_ATTRIBUTE }) @Retention(RetentionPolicy.SOURCE) @interface ValueType {} static final int VALUE_TYPE_RAW = 1; static final int VALUE_TYPE_COMPLEX_UNIT = 2; static final int VALUE_TYPE_RESOURCE = 3; static final int VALUE_TYPE_ATTRIBUTE = 4; /** @hide **/ @IntDef(flag = true, value = { FLAG_REAPPLY_DISALLOWED, FLAG_WIDGET_IS_COLLECTION_CHILD, FLAG_USE_LIGHT_BACKGROUND_LAYOUT }) @Retention(RetentionPolicy.SOURCE) public @interface ApplyFlags {} /** * Whether reapply is disallowed on this remoteview. This maybe be true if some actions modify * the layout in a way that isn't recoverable, since views are being removed. * @hide */ public static final int FLAG_REAPPLY_DISALLOWED = 1; /** * This flag indicates whether this RemoteViews object is being created from a * RemoteViewsService for use as a child of a widget collection. This flag is used * to determine whether or not certain features are available, in particular, * setting on click extras and setting on click pending intents. The former is enabled, * and the latter disabled when this flag is true. * @hide */ public static final int FLAG_WIDGET_IS_COLLECTION_CHILD = 2; /** * When this flag is set, the views is inflated with {@link #mLightBackgroundLayoutId} instead * of {link #mLayoutId} * @hide */ public static final int FLAG_USE_LIGHT_BACKGROUND_LAYOUT = 4; /** * This mask determines which flags are propagated to nested RemoteViews (either added by * addView, or set as portrait/landscape/sized RemoteViews). */ static final int FLAG_MASK_TO_PROPAGATE = FLAG_WIDGET_IS_COLLECTION_CHILD | FLAG_USE_LIGHT_BACKGROUND_LAYOUT; /** * A ReadWriteHelper which has the same behavior as ReadWriteHelper.DEFAULT, but which is * intentionally a different instance in order to trick Bundle reader so that it doesn't allow * lazy initialization. */ private static final Parcel.ReadWriteHelper ALTERNATIVE_DEFAULT = new Parcel.ReadWriteHelper(); /** * Used to restrict the views which can be inflated * * @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class) */ private static final LayoutInflater.Filter INFLATER_FILTER = (clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class); /** * The maximum waiting time for remote adapter conversion in milliseconds * * @hide */ private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 20_000; /** * Application that hosts the remote views. * * @hide */ @UnsupportedAppUsage public ApplicationInfo mApplication; /** * The resource ID of the layout file. (Added to the parcel) */ @UnsupportedAppUsage private int mLayoutId; /** * The resource ID of the layout file in dark text mode. (Added to the parcel) */ private int mLightBackgroundLayoutId = 0; /** * An array of actions to perform on the view tree once it has been * inflated */ @UnsupportedAppUsage private ArrayList mActions; /** * Maps bitmaps to unique indicies to avoid Bitmap duplication. */ @UnsupportedAppUsage private BitmapCache mBitmapCache = new BitmapCache(); /** * Maps Intent ID to RemoteCollectionItems to avoid duplicate items */ private @NonNull RemoteCollectionCache mCollectionCache = new RemoteCollectionCache(); /** Cache of ApplicationInfos used by collection items. */ private ApplicationInfoCache mApplicationInfoCache = new ApplicationInfoCache(); /** * Indicates whether or not this RemoteViews object is contained as a child of any other * RemoteViews. */ private boolean mIsRoot = true; /** * Constants to whether or not this RemoteViews is composed of a landscape and portrait * RemoteViews. */ 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 * {@link #RemoteViews(RemoteViews, RemoteViews)} to keep track of the landscape and portrait * RemoteViews. */ 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 mSizedRemoteViews = null; /** * Ideal size for this RemoteViews. * * Only to be used on children views used in a {@link RemoteViews} with * {@link RemoteViews#hasSizedRemoteViews()}. */ private SizeF mIdealSize = null; @ApplyFlags private int mApplyFlags = 0; /** * Id to use to override the ID of the top-level view in this RemoteViews. * * Only used if this RemoteViews is defined from a XML layout value. */ private int mViewId = View.NO_ID; /** * Id used to uniquely identify a {@link RemoteViews} instance coming from a given provider. */ private long mProviderInstanceId = -1; /** Class cookies of the Parcel this instance was read from. */ private Map mClassCookies; /** * {@link LayoutInflater.Factory2} which will be passed into a {@link LayoutInflater} instance * used by this class. */ @Nullable private LayoutInflater.Factory2 mLayoutInflaterFactory2; /** * Indicates whether this {@link RemoteViews} was instantiated with a {@link DrawInstructions} * object. {@link DrawInstructions} serves as an alternative protocol for the host process * to render. */ private boolean mHasDrawInstructions; @Nullable private SparseArray mPendingIntentTemplate; @Nullable private SparseArray mFillInIntent; private static final InteractionHandler DEFAULT_INTERACTION_HANDLER = (view, pendingIntent, response) -> startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)); private static final ArrayMap sMethods = new ArrayMap<>(); /** * This key is used to perform lookups in sMethods without causing allocations. */ private static final MethodKey sLookupKey = new MethodKey(); /** * @hide */ public void setRemoteInputs(@IdRes int viewId, RemoteInput[] remoteInputs) { mActions.add(new SetRemoteInputsAction(viewId, remoteInputs)); } /** * Sets {@link LayoutInflater.Factory2} to be passed into {@link LayoutInflater} used * by this class instance. It has to be set before the views are inflated to have any effect. * * The factory callbacks will be called on the background thread so the implementation needs * to be thread safe. * * @hide */ public void setLayoutInflaterFactory(@Nullable LayoutInflater.Factory2 factory) { mLayoutInflaterFactory2 = factory; } /** * Returns currently set {@link LayoutInflater.Factory2}. * * @hide */ @Nullable public LayoutInflater.Factory2 getLayoutInflaterFactory() { return mLayoutInflaterFactory2; } /** * Reduces all images and ensures that they are all below the given sizes. * * @param maxWidth the maximum width allowed * @param maxHeight the maximum height allowed * * @hide */ public void reduceImageSizes(int maxWidth, int maxHeight) { ArrayList cache = mBitmapCache.mBitmaps; for (int i = 0; i < cache.size(); i++) { Bitmap bitmap = cache.get(i); cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight)); } } /** * Sets an integer tag to the view. * * @hide */ public void setIntTag(@IdRes int viewId, @IdRes int key, int tag) { addAction(new SetIntTagAction(viewId, key, tag)); } /** * Set that it is disallowed to reapply another remoteview with the same layout as this view. * This should be done if an action is destroying the view tree of the base layout. * * @hide */ public void addFlags(@ApplyFlags int flags) { mApplyFlags = mApplyFlags | flags; int flagsToPropagate = flags & FLAG_MASK_TO_PROPAGATE; if (flagsToPropagate != 0) { if (hasSizedRemoteViews()) { for (RemoteViews remoteView : mSizedRemoteViews) { remoteView.addFlags(flagsToPropagate); } } else if (hasLandscapeAndPortraitLayouts()) { mLandscape.addFlags(flagsToPropagate); mPortrait.addFlags(flagsToPropagate); } } } /** * @hide */ public boolean hasFlags(@ApplyFlags int flag) { return (mApplyFlags & flag) == flag; } /** * Stores information related to reflection method lookup. */ static class MethodKey { public Class targetClass; public Class paramClass; public String methodName; @Override public boolean equals(@Nullable Object o) { if (!(o instanceof MethodKey)) { return false; } MethodKey p = (MethodKey) o; return Objects.equals(p.targetClass, targetClass) && Objects.equals(p.paramClass, paramClass) && Objects.equals(p.methodName, methodName); } @Override public int hashCode() { return Objects.hashCode(targetClass) ^ Objects.hashCode(paramClass) ^ Objects.hashCode(methodName); } public void set(Class targetClass, Class paramClass, String methodName) { this.targetClass = targetClass; this.paramClass = paramClass; this.methodName = methodName; } } /** * Stores information related to reflection method lookup result. */ static class MethodArgs { public MethodHandle syncMethod; public MethodHandle asyncMethod; public String asyncMethodName; } /** * This annotation indicates that a subclass of View is allowed to be used * with the {@link RemoteViews} mechanism. */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface RemoteView { } /** * Exception to send when something goes wrong executing an action * */ public static class ActionException extends RuntimeException { public ActionException(Exception ex) { super(ex); } public ActionException(String message) { super(message); } /** * @hide */ public ActionException(Throwable t) { super(t); } } /** * Handler for view interactions (such as clicks) within a RemoteViews. * * @hide */ public interface InteractionHandler { /** * Invoked when the user performs an interaction on the View. * * @param view the View with which the user interacted * @param pendingIntent the base PendingIntent associated with the view * @param response the response to the interaction, which knows how to fill in the * attached PendingIntent * * @hide */ boolean onInteraction( View view, PendingIntent pendingIntent, RemoteResponse response); } /** * Base class for all actions that can be performed on an * inflated view. * * SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!! */ private abstract static class Action { @IdRes @UnsupportedAppUsage int mViewId; public abstract void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException; public static final int MERGE_REPLACE = 0; public static final int MERGE_APPEND = 1; public static final int MERGE_IGNORE = 2; public void setHierarchyRootData(HierarchyRootData root) { // Do nothing } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public int mergeBehavior() { return MERGE_REPLACE; } public abstract int getActionTag(); public String getUniqueKey() { return (getActionTag() + "_" + mViewId); } /** * This is called on the background thread. It should perform any non-ui computations * and return the final action which will run on the UI thread. * Override this if some of the tasks can be performed async. */ public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { return this; } public boolean prefersAsyncApply() { return false; } /** See {@link RemoteViews#visitUris(Consumer)}. **/ public void visitUris(@NonNull Consumer visitor) { // Nothing to visit by default. } public abstract void writeToParcel(Parcel dest, int flags); } /** * Action class used during async inflation of RemoteViews. Subclasses are not parcelable. */ private abstract static class RuntimeAction extends Action { @Override public final int getActionTag() { return 0; } @Override public final void writeToParcel(Parcel dest, int flags) { throw new UnsupportedOperationException(); } } // Constant used during async execution. It is not parcelable. private static final Action ACTION_NOOP = new RuntimeAction() { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { } }; /** * Merges the passed RemoteViews actions with this RemoteViews actions according to * action-specific merge rules. * * @param newRv * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void mergeRemoteViews(RemoteViews newRv) { if (newRv == null) return; // We first copy the new RemoteViews, as the process of merging modifies the way the actions // reference the bitmap cache. We don't want to modify the object as it may need to // be merged and applied multiple times. RemoteViews copy = new RemoteViews(newRv); HashMap map = new HashMap(); if (mActions == null) { mActions = new ArrayList(); } int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); map.put(a.getUniqueKey(), a); } ArrayList newActions = copy.mActions; if (newActions == null) return; count = newActions.size(); for (int i = 0; i < count; i++) { Action a = newActions.get(i); String key = newActions.get(i).getUniqueKey(); int mergeBehavior = newActions.get(i).mergeBehavior(); if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) { mActions.remove(map.get(key)); map.remove(key); } // If the merge behavior is ignore, we don't bother keeping the extra action if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) { mActions.add(a); } } // Because pruning can remove the need for bitmaps, we reconstruct the caches. reconstructCaches(); } /** * Return {@code true} only if this {@code RemoteViews} is a legacy list widget that uses * {@code Intent} for inflating child entries. * * @hide */ public boolean isLegacyListRemoteViews() { return mCollectionCache.mIdToUriMapping.size() > 0; } /** * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission * grants will need to be issued to ensure the recipient of this object is able to render its * contents. * See b/281044385 for more context and examples about what happens when this isn't done * correctly. * * @hide */ public void visitUris(@NonNull Consumer visitor) { if (mActions != null) { for (int i = 0; i < mActions.size(); i++) { mActions.get(i).visitUris(visitor); } } if (mSizedRemoteViews != null) { for (int i = 0; i < mSizedRemoteViews.size(); i++) { mSizedRemoteViews.get(i).visitUris(visitor); } } if (mLandscape != null) { mLandscape.visitUris(visitor); } if (mPortrait != null) { mPortrait.visitUris(visitor); } } /** * @hide * @return True if there is a change */ public boolean replaceRemoteCollections(int viewId) { boolean isActionReplaced = false; if (mActions != null) { for (int i = 0; i < mActions.size(); i++) { Action action = mActions.get(i); if (action instanceof SetRemoteCollectionItemListAdapterAction itemsAction && itemsAction.mViewId == viewId && itemsAction.mServiceIntent != null) { SetRemoteCollectionItemListAdapterAction newCollectionAction = new SetRemoteCollectionItemListAdapterAction( itemsAction.mViewId, itemsAction.mServiceIntent); newCollectionAction.mIntentId = itemsAction.mIntentId; newCollectionAction.mIsReplacedIntoAction = true; mActions.set(i, newCollectionAction); isActionReplaced = true; } else if (action instanceof SetRemoteViewsAdapterIntent intentAction && intentAction.mViewId == viewId) { mActions.set(i, new SetRemoteCollectionItemListAdapterAction( intentAction.mViewId, intentAction.mIntent)); isActionReplaced = true; } else if (action instanceof ViewGroupActionAdd groupAction && groupAction.mNestedViews != null) { isActionReplaced |= groupAction.mNestedViews.replaceRemoteCollections(viewId); } } } if (mSizedRemoteViews != null) { for (int i = 0; i < mSizedRemoteViews.size(); i++) { isActionReplaced |= mSizedRemoteViews.get(i).replaceRemoteCollections(viewId); } } if (mLandscape != null) { isActionReplaced |= mLandscape.replaceRemoteCollections(viewId); } if (mPortrait != null) { isActionReplaced |= mPortrait.replaceRemoteCollections(viewId); } return isActionReplaced; } /** * @return True if has set remote adapter using service intent * @hide */ public boolean hasLegacyLists() { if (mActions != null) { for (int i = 0; i < mActions.size(); i++) { Action action = mActions.get(i); if ((action instanceof SetRemoteCollectionItemListAdapterAction itemsAction && itemsAction.mServiceIntent != null) || (action instanceof SetRemoteViewsAdapterIntent intentAction && intentAction.mIntent != null) || (action instanceof ViewGroupActionAdd groupAction && groupAction.mNestedViews != null && groupAction.mNestedViews.hasLegacyLists())) { return true; } } } if (mSizedRemoteViews != null) { for (int i = 0; i < mSizedRemoteViews.size(); i++) { if (mSizedRemoteViews.get(i).hasLegacyLists()) { return true; } } } if (mLandscape != null && mLandscape.hasLegacyLists()) { return true; } return mPortrait != null && mPortrait.hasLegacyLists(); } private static void visitIconUri(Icon icon, @NonNull Consumer visitor) { if (icon != null && (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) { visitor.accept(icon.getUri()); } } private static class RemoteViewsContextWrapper extends ContextWrapper { private final Context mContextForResources; RemoteViewsContextWrapper(Context context, Context contextForResources) { super(context); mContextForResources = contextForResources; } @Override public Resources getResources() { return mContextForResources.getResources(); } @Override public Resources.Theme getTheme() { return mContextForResources.getTheme(); } @Override public String getPackageName() { return mContextForResources.getPackageName(); } @Override public UserHandle getUser() { return mContextForResources.getUser(); } @Override public int getUserId() { return mContextForResources.getUserId(); } @Override public boolean isRestricted() { // Override isRestricted and direct to resource's implementation. The isRestricted is // used for determining the risky resources loading, e.g. fonts, thus direct to context // for resource. return mContextForResources.isRestricted(); } } private static class SetEmptyView extends Action { int mEmptyViewId; SetEmptyView(@IdRes int viewId, @IdRes int emptyViewId) { this.mViewId = viewId; this.mEmptyViewId = emptyViewId; } SetEmptyView(Parcel in) { this.mViewId = in.readInt(); this.mEmptyViewId = in.readInt(); } public void writeToParcel(Parcel out, int flags) { out.writeInt(this.mViewId); out.writeInt(this.mEmptyViewId); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View view = root.findViewById(mViewId); if (!(view instanceof AdapterView)) return; AdapterView adapterView = (AdapterView) view; final View emptyView = root.findViewById(mEmptyViewId); if (emptyView == null) return; adapterView.setEmptyView(emptyView); } @Override public int getActionTag() { return SET_EMPTY_VIEW_ACTION_TAG; } } private static class SetPendingIntentTemplate extends Action { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) PendingIntent mPendingIntentTemplate; public SetPendingIntentTemplate(@IdRes int id, PendingIntent pendingIntentTemplate) { this.mViewId = id; this.mPendingIntentTemplate = pendingIntentTemplate; } public SetPendingIntentTemplate(Parcel parcel) { mViewId = parcel.readInt(); mPendingIntentTemplate = PendingIntent.readPendingIntentOrNullFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); PendingIntent.writePendingIntentOrNullToParcel(mPendingIntentTemplate, dest); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; // If the view isn't an AdapterView, setting a PendingIntent template doesn't make sense if (target instanceof AdapterView) { AdapterView av = (AdapterView) target; // The PendingIntent template is stored in the view's tag. OnItemClickListener listener = (parent, view, position, id) -> { RemoteResponse response = findRemoteResponseTag(view); if (response != null) { response.handleViewInteraction(view, params.handler); } }; av.setOnItemClickListener(listener); av.setTag(mPendingIntentTemplate); } else { Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" + "an AdapterView (id: " + mViewId + ")"); return; } } @Nullable private RemoteResponse findRemoteResponseTag(@Nullable View rootView) { if (rootView == null) return null; ArrayDeque viewsToCheck = new ArrayDeque<>(); viewsToCheck.addLast(rootView); while (!viewsToCheck.isEmpty()) { View view = viewsToCheck.removeFirst(); Object tag = view.getTag(R.id.fillInIntent); if (tag instanceof RemoteResponse) return (RemoteResponse) tag; if (!(view instanceof ViewGroup)) continue; ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { viewsToCheck.addLast(viewGroup.getChildAt(i)); } } return null; } @Override public int getActionTag() { return SET_PENDING_INTENT_TEMPLATE_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mPendingIntentTemplate.visitUris(visitor); } } /** * Cache of {@link ApplicationInfo}s that can be used to ensure that the same * {@link ApplicationInfo} instance is used throughout the RemoteViews. */ private static class ApplicationInfoCache { private final Map, ApplicationInfo> mPackageUserToApplicationInfo; ApplicationInfoCache() { mPackageUserToApplicationInfo = new ArrayMap<>(); } /** * Adds the {@link ApplicationInfo} to the cache if it's not present. Returns either the * provided {@code applicationInfo} or a previously added value with the same package name * and uid. */ @Nullable ApplicationInfo getOrPut(@Nullable ApplicationInfo applicationInfo) { Pair key = getPackageUserKey(applicationInfo); if (key == null) return null; return mPackageUserToApplicationInfo.computeIfAbsent(key, ignored -> applicationInfo); } /** Puts the {@link ApplicationInfo} in the cache, replacing any previously stored value. */ void put(@Nullable ApplicationInfo applicationInfo) { Pair key = getPackageUserKey(applicationInfo); if (key == null) return; mPackageUserToApplicationInfo.put(key, applicationInfo); } /** * Returns the currently stored {@link ApplicationInfo} from the cache matching * {@code applicationInfo}, or null if there wasn't any. */ @Nullable ApplicationInfo get(@Nullable ApplicationInfo applicationInfo) { Pair key = getPackageUserKey(applicationInfo); if (key == null) return null; return mPackageUserToApplicationInfo.get(key); } } private class SetRemoteCollectionItemListAdapterAction extends Action { private @Nullable RemoteCollectionItems mItems; final Intent mServiceIntent; int mIntentId = -1; boolean mIsReplacedIntoAction = false; SetRemoteCollectionItemListAdapterAction(@IdRes int id, @NonNull RemoteCollectionItems items) { mViewId = id; items.setHierarchyRootData(getHierarchyRootData()); mItems = items; mServiceIntent = null; } SetRemoteCollectionItemListAdapterAction(@IdRes int id, Intent intent) { mViewId = id; mItems = null; mServiceIntent = intent; } SetRemoteCollectionItemListAdapterAction(Parcel parcel) { mViewId = parcel.readInt(); mIntentId = parcel.readInt(); mIsReplacedIntoAction = parcel.readBoolean(); mServiceIntent = parcel.readTypedObject(Intent.CREATOR); mItems = mServiceIntent != null ? null : new RemoteCollectionItems(parcel, getHierarchyRootData()); } @Override public void setHierarchyRootData(HierarchyRootData rootData) { if (mItems != null) { mItems.setHierarchyRootData(rootData); return; } if (mIntentId != -1) { // Set the root data for items in the cache instead mCollectionCache.setHierarchyDataForId(mIntentId, rootData); } } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mIntentId); dest.writeBoolean(mIsReplacedIntoAction); dest.writeTypedObject(mServiceIntent, flags); if (mItems != null) { mItems.writeToParcel(dest, flags, /* attached= */ true); } } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { View target = root.findViewById(mViewId); if (target == null) return; RemoteCollectionItems items = mIntentId == -1 ? mItems == null ? new RemoteCollectionItems.Builder().build() : mItems : mCollectionCache.getItemsForId(mIntentId); // Ensure that we are applying to an AppWidget root if (!(rootParent instanceof AppWidgetHostView)) { Log.e(LOG_TAG, "setRemoteAdapter can only be used for " + "AppWidgets (root id: " + mViewId + ")"); return; } if (!(target instanceof AdapterView)) { Log.e(LOG_TAG, "Cannot call setRemoteAdapter on a view which is not " + "an AdapterView (id: " + mViewId + ")"); return; } AdapterView adapterView = (AdapterView) target; Adapter adapter = adapterView.getAdapter(); // We can reuse the adapter if it's a RemoteCollectionItemsAdapter and the view type // count hasn't increased. Note that AbsListView allocates a fixed size array for view // recycling in setAdapter, so we must call setAdapter again if the number of view types // increases. if (adapter instanceof RemoteCollectionItemsAdapter && adapter.getViewTypeCount() >= items.getViewTypeCount()) { try { ((RemoteCollectionItemsAdapter) adapter).setData( items, params.handler, params.colorResources); } catch (Throwable throwable) { // setData should never failed with the validation in the items builder, but if // it does, catch and rethrow. throw new ActionException(throwable); } return; } try { adapterView.setAdapter(new RemoteCollectionItemsAdapter(items, params.handler, params.colorResources)); } catch (Throwable throwable) { // This could throw if the AdapterView somehow doesn't accept BaseAdapter due to // a type error. throw new ActionException(throwable); } } @Override public int getActionTag() { return SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG; } @Override public String getUniqueKey() { return (SET_REMOTE_ADAPTER_TAG + "_" + mViewId); } @Override public void visitUris(@NonNull Consumer visitor) { if (mIntentId != -1 || mItems == null) { return; } mItems.visitUris(visitor); } } /** * The maximum size for RemoteViews with converted RemoteCollectionItemsAdapter. * When converting RemoteViewsAdapter to RemoteCollectionItemsAdapter, we want to put size * limits on each unique RemoteCollectionItems in order to not exceed the transaction size limit * for each parcel (typically 1 MB). We leave a certain ratio of the maximum size as a buffer * for missing calculations of certain parameters (e.g. writing a RemoteCollectionItems to the * parcel will write its Id array as well, but that is missing when writing itschild RemoteViews * directly to the parcel as we did in RemoteViewsService) * * @hide */ private static final int MAX_SINGLE_PARCEL_SIZE = (int) (1_000_000 * 0.8); /** * @hide */ public CompletableFuture collectAllIntents() { return mCollectionCache.collectAllIntentsNoComplete(this); } private class RemoteCollectionCache { private final SparseArray mIdToUriMapping = new SparseArray<>(); private final Map mUriToCollectionMapping = new HashMap<>(); RemoteCollectionCache() { } RemoteCollectionCache(RemoteCollectionCache src) { for (int i = 0; i < src.mIdToUriMapping.size(); i++) { String uri = src.mIdToUriMapping.valueAt(i); mIdToUriMapping.put(src.mIdToUriMapping.keyAt(i), uri); mUriToCollectionMapping.put(uri, src.mUriToCollectionMapping.get(uri)); } } RemoteCollectionCache(Parcel in) { int cacheSize = in.readInt(); HierarchyRootData currentRootData = new HierarchyRootData(mBitmapCache, this, mApplicationInfoCache, mClassCookies); for (int i = 0; i < cacheSize; i++) { int intentId = in.readInt(); String intentUri = in.readString8(); RemoteCollectionItems items = new RemoteCollectionItems(in, currentRootData); mIdToUriMapping.put(intentId, intentUri); mUriToCollectionMapping.put(intentUri, items); } } void setHierarchyDataForId(int intentId, HierarchyRootData data) { String uri = mIdToUriMapping.get(intentId); if (mUriToCollectionMapping.get(uri) == null) { Log.e(LOG_TAG, "Error setting hierarchy data for id=" + intentId); return; } RemoteCollectionItems items = mUriToCollectionMapping.get(uri); items.setHierarchyRootData(data); } RemoteCollectionItems getItemsForId(int intentId) { String uri = mIdToUriMapping.get(intentId); return mUriToCollectionMapping.get(uri); } public @NonNull CompletableFuture collectAllIntentsNoComplete( @NonNull RemoteViews inViews) { SparseArray idToIntentMapping = new SparseArray<>(); // Collect the number of uinque Intent (which is equal to the number of new connections // to make) for size allocation and exclude certain collections from being written to // the parcel to better estimate the space left for reallocation. collectAllIntentsInternal(inViews, idToIntentMapping); // Calculate the individual size here int numOfIntents = idToIntentMapping.size(); if (numOfIntents == 0) { Log.e(LOG_TAG, "Possibly notifying updates for nonexistent view Id"); return CompletableFuture.completedFuture(null); } Parcel sizeTestParcel = Parcel.obtain(); // Write self RemoteViews to the parcel, which includes the actions/bitmaps/collection // cache to see how much space is left for the RemoteCollectionItems that are to be // updated. RemoteViews.this.writeToParcel(sizeTestParcel, /* flags= */ 0, /* intentsToIgnore= */ idToIntentMapping); int remainingSize = MAX_SINGLE_PARCEL_SIZE - sizeTestParcel.dataSize(); sizeTestParcel.recycle(); int individualSize = remainingSize < 0 ? 0 : remainingSize / numOfIntents; return connectAllUniqueIntents(individualSize, idToIntentMapping); } private void collectAllIntentsInternal(@NonNull RemoteViews inViews, @NonNull SparseArray idToIntentMapping) { if (inViews.hasSizedRemoteViews()) { for (RemoteViews remoteViews : inViews.mSizedRemoteViews) { collectAllIntentsInternal(remoteViews, idToIntentMapping); } } else if (inViews.hasLandscapeAndPortraitLayouts()) { collectAllIntentsInternal(inViews.mLandscape, idToIntentMapping); collectAllIntentsInternal(inViews.mPortrait, idToIntentMapping); } else if (inViews.mActions != null) { for (Action action : inViews.mActions) { if (action instanceof SetRemoteCollectionItemListAdapterAction rca) { // Deal with the case where the intent is replaced into the action list if (rca.mIntentId != -1 && !rca.mIsReplacedIntoAction) { continue; } if (rca.mIntentId != -1 && rca.mIsReplacedIntoAction) { rca.mIsReplacedIntoAction = false; // Avoid redundant connections for the same intent. Also making sure // that the number of connections we are making is always equal to the // nmuber of unique intents that are being used for the updates. if (idToIntentMapping.contains(rca.mIntentId)) { continue; } idToIntentMapping.put(rca.mIntentId, rca.mServiceIntent); rca.mItems = null; continue; } // Differentiate between the normal collection actions and the ones with // intents. if (rca.mServiceIntent != null) { final String uri = rca.mServiceIntent.toUri(0); int index = mIdToUriMapping.indexOfValueByValue(uri); if (index == -1) { int newIntentId = mIdToUriMapping.size(); rca.mIntentId = newIntentId; mIdToUriMapping.put(newIntentId, uri); } else { rca.mIntentId = mIdToUriMapping.keyAt(index); rca.mItems = null; continue; } idToIntentMapping.put(rca.mIntentId, rca.mServiceIntent); rca.mItems = null; } else { for (RemoteViews views : rca.mItems.mViews) { collectAllIntentsInternal(views, idToIntentMapping); } } } else if (action instanceof ViewGroupActionAdd vgaa && vgaa.mNestedViews != null) { collectAllIntentsInternal(vgaa.mNestedViews, idToIntentMapping); } } } } private @NonNull CompletableFuture connectAllUniqueIntents(int individualSize, @NonNull SparseArray idToIntentMapping) { List> intentFutureList = new ArrayList<>(); for (int i = 0; i < idToIntentMapping.size(); i++) { String currentIntentUri = mIdToUriMapping.get(idToIntentMapping.keyAt(i)); Intent currentIntent = idToIntentMapping.valueAt(i); intentFutureList.add(getItemsFutureFromIntentWithTimeout(currentIntent, individualSize) .thenAccept(items -> { items.setHierarchyRootData(getHierarchyRootData()); mUriToCollectionMapping.put(currentIntentUri, items); })); } return CompletableFuture.allOf(intentFutureList.toArray(CompletableFuture[]::new)); } private static CompletableFuture getItemsFutureFromIntentWithTimeout( Intent intent, int individualSize) { if (intent == null) { Log.e(LOG_TAG, "Null intent received when generating adapter future"); return CompletableFuture.completedFuture(new RemoteCollectionItems .Builder().build()); } final Context context = ActivityThread.currentApplication(); final CompletableFuture result = new CompletableFuture<>(); context.bindService(intent, Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE), result.defaultExecutor(), new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { RemoteCollectionItems items; try { items = IRemoteViewsFactory.Stub.asInterface(iBinder) .getRemoteCollectionItems(individualSize); } catch (RemoteException re) { items = new RemoteCollectionItems.Builder().build(); Log.e(LOG_TAG, "Error getting collection items from the" + " factory", re); } finally { context.unbindService(this); } result.complete(items); } @Override public void onServiceDisconnected(ComponentName componentName) { } }); result.completeOnTimeout( new RemoteCollectionItems.Builder().build(), MAX_ADAPTER_CONVERSION_WAITING_TIME_MS, TimeUnit.MILLISECONDS); return result; } public void writeToParcel(Parcel out, int flags, @Nullable SparseArray intentsToIgnore) { out.writeInt(mIdToUriMapping.size()); for (int i = 0; i < mIdToUriMapping.size(); i++) { int currentIntentId = mIdToUriMapping.keyAt(i); if (intentsToIgnore != null && intentsToIgnore.contains(currentIntentId)) { // Skip writing collections that are to be updated in the following steps to // better estimate the RemoteViews size. continue; } out.writeInt(currentIntentId); String intentUri = mIdToUriMapping.valueAt(i); out.writeString8(intentUri); mUriToCollectionMapping.get(intentUri).writeToParcel(out, flags, true); } } } private class SetRemoteViewsAdapterIntent extends Action { Intent mIntent; boolean mIsAsync = false; public SetRemoteViewsAdapterIntent(@IdRes int id, Intent intent) { this.mViewId = id; this.mIntent = intent; } public SetRemoteViewsAdapterIntent(Parcel parcel) { mViewId = parcel.readInt(); mIntent = parcel.readTypedObject(Intent.CREATOR); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeTypedObject(mIntent, flags); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; // Ensure that we are applying to an AppWidget root if (!(rootParent instanceof AppWidgetHostView)) { Log.e(LOG_TAG, "setRemoteAdapter can only be used for " + "AppWidgets (root id: " + mViewId + ")"); return; } // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) { Log.e(LOG_TAG, "Cannot setRemoteAdapter on a view which is not " + "an AbsListView or AdapterViewAnimator (id: " + mViewId + ")"); return; } // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent // RemoteViewsService AppWidgetHostView host = (AppWidgetHostView) rootParent; mIntent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId()) .putExtra(EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND, hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT)); if (target instanceof AbsListView) { AbsListView v = (AbsListView) target; v.setRemoteViewsAdapter(mIntent, mIsAsync); v.setRemoteViewsInteractionHandler(params.handler); } else if (target instanceof AdapterViewAnimator) { AdapterViewAnimator v = (AdapterViewAnimator) target; v.setRemoteViewsAdapter(mIntent, mIsAsync); v.setRemoteViewsOnClickHandler(params.handler); } } @Override public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(mViewId, mIntent); copy.mIsAsync = true; return copy; } @Override public int getActionTag() { return SET_REMOTE_VIEW_ADAPTER_INTENT_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mIntent.visitUris(visitor); } } /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} * to launch the provided {@link PendingIntent}. */ private class SetOnClickResponse extends Action { final RemoteResponse mResponse; SetOnClickResponse(@IdRes int id, RemoteResponse response) { this.mViewId = id; this.mResponse = response; } SetOnClickResponse(Parcel parcel) { mViewId = parcel.readInt(); mResponse = new RemoteResponse(); mResponse.readFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); mResponse.writeToParcel(dest, flags); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { if (hasDrawInstructions() && root instanceof RemoteComposePlayer) { return; } final View target = root.findViewById(mViewId); if (target == null) return; if (mResponse.mPendingIntent != null) { // If the view is an AdapterView, setting a PendingIntent on click doesn't make // much sense, do they mean to set a PendingIntent template for the // AdapterView's children? if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) { Log.w(LOG_TAG, "Cannot SetOnClickResponse for collection item " + "(id: " + mViewId + ")"); ApplicationInfo appInfo = root.getContext().getApplicationInfo(); // We let this slide for HC and ICS so as to not break compatibility. It should // have been disabled from the outset, but was left open by accident. if (appInfo != null && appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) { return; } } target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent); } else if (mResponse.mFillIntent != null) { if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) { Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " + "only from RemoteViewsFactory (ie. on collection items)."); return; } if (target == root) { // Target is a root node of an AdapterView child. Set the response in the tag. // Actual click handling is done by OnItemClickListener in // SetPendingIntentTemplate, which uses this tag information. target.setTagInternal(com.android.internal.R.id.fillInIntent, mResponse); return; } } else { // No intent to apply, clear the listener and any tags that were previously set. target.setOnClickListener(null); target.setTagInternal(R.id.pending_intent_tag, null); target.setTagInternal(com.android.internal.R.id.fillInIntent, null); return; } target.setOnClickListener(v -> mResponse.handleViewInteraction(v, params.handler)); } @Override public int getActionTag() { return SET_ON_CLICK_RESPONSE_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mResponse.visitUris(visitor); } } /** Helper action to configure handwriting delegation via {@link PendingIntent}. */ private class SetOnStylusHandwritingResponse extends Action { final PendingIntent mPendingIntent; SetOnStylusHandwritingResponse(@IdRes int id, @Nullable PendingIntent pendingIntent) { this.mViewId = id; this.mPendingIntent = pendingIntent; } SetOnStylusHandwritingResponse(@NonNull Parcel parcel) { mViewId = parcel.readInt(); mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); } public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mViewId); PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) { Log.w(LOG_TAG, "Cannot use setOnStylusHandwritingPendingIntent for collection item " + "(id: " + mViewId + ")"); return; } if (mPendingIntent != null) { RemoteResponse response = RemoteResponse.fromPendingIntent(mPendingIntent); target.setHandwritingDelegatorCallback( () -> response.handleViewInteraction(target, params.handler)); target.setAllowedHandwritingDelegatePackage(mPendingIntent.getCreatorPackage()); } else { target.setHandwritingDelegatorCallback(null); target.setAllowedHandwritingDelegatePackage(null); } } @Override public int getActionTag() { return SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mPendingIntent.visitUris(visitor); } } /** * Equivalent to calling * {@link android.widget.CompoundButton#setOnCheckedChangeListener( * android.widget.CompoundButton.OnCheckedChangeListener)} * to launch the provided {@link PendingIntent}. */ private class SetOnCheckedChangeResponse extends Action { private final RemoteResponse mResponse; SetOnCheckedChangeResponse(@IdRes int id, RemoteResponse response) { this.mViewId = id; this.mResponse = response; } SetOnCheckedChangeResponse(Parcel parcel) { mViewId = parcel.readInt(); mResponse = new RemoteResponse(); mResponse.readFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); mResponse.writeToParcel(dest, flags); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; if (!(target instanceof CompoundButton)) { Log.w(LOG_TAG, "setOnCheckedChange methods cannot be used on " + "non-CompoundButton child (id: " + mViewId + ")"); return; } CompoundButton button = (CompoundButton) target; if (mResponse.mPendingIntent != null) { // setOnCheckedChangePendingIntent cannot be used with collection children, which // must use setOnCheckedChangeFillInIntent instead. if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) { Log.w(LOG_TAG, "Cannot setOnCheckedChangePendingIntent for collection item " + "(id: " + mViewId + ")"); return; } target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent); } else if (mResponse.mFillIntent != null) { if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) { Log.e(LOG_TAG, "The method setOnCheckedChangeFillInIntent is available " + "only from RemoteViewsFactory (ie. on collection items)."); return; } } else { // No intent to apply, clear any existing listener or tag. button.setOnCheckedChangeListener(null); button.setTagInternal(R.id.remote_checked_change_listener_tag, null); return; } OnCheckedChangeListener onCheckedChangeListener = (v, isChecked) -> mResponse.handleViewInteraction(v, params.handler); button.setTagInternal(R.id.remote_checked_change_listener_tag, onCheckedChangeListener); button.setOnCheckedChangeListener(onCheckedChangeListener); } @Override public int getActionTag() { return SET_ON_CHECKED_CHANGE_RESPONSE_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mResponse.visitUris(visitor); } } /** @hide **/ public static Rect getSourceBounds(View v) { final float appScale = v.getContext().getResources() .getCompatibilityInfo().applicationScale; final int[] pos = new int[2]; v.getLocationOnScreen(pos); final Rect rect = new Rect(); rect.left = (int) (pos[0] * appScale + 0.5f); rect.top = (int) (pos[1] * appScale + 0.5f); rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f); rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f); return rect; } @Nullable private static Class getParameterType(int type) { switch (type) { case BaseReflectionAction.BOOLEAN: return boolean.class; case BaseReflectionAction.BYTE: return byte.class; case BaseReflectionAction.SHORT: return short.class; case BaseReflectionAction.INT: return int.class; case BaseReflectionAction.LONG: return long.class; case BaseReflectionAction.FLOAT: return float.class; case BaseReflectionAction.DOUBLE: return double.class; case BaseReflectionAction.CHAR: return char.class; case BaseReflectionAction.STRING: return String.class; case BaseReflectionAction.CHAR_SEQUENCE: return CharSequence.class; case BaseReflectionAction.URI: return Uri.class; case BaseReflectionAction.BITMAP: return Bitmap.class; case BaseReflectionAction.BUNDLE: return Bundle.class; case BaseReflectionAction.INTENT: return Intent.class; case BaseReflectionAction.COLOR_STATE_LIST: return ColorStateList.class; case BaseReflectionAction.ICON: return Icon.class; case BaseReflectionAction.BLEND_MODE: return BlendMode.class; default: return null; } } @Nullable private static MethodHandle getMethod(View view, String methodName, Class paramType, boolean async) { MethodArgs result; Class klass = view.getClass(); synchronized (sMethods) { // The key is defined by the view class, param class and method name. sLookupKey.set(klass, paramType, methodName); result = sMethods.get(sLookupKey); if (result == null) { Method method; try { if (paramType == null) { method = klass.getMethod(methodName); } else { method = klass.getMethod(methodName, paramType); } if (!method.isAnnotationPresent(RemotableViewMethod.class)) { throw new ActionException("view: " + klass.getName() + " can't use method with RemoteViews: " + methodName + getParameters(paramType)); } result = new MethodArgs(); result.syncMethod = MethodHandles.publicLookup().unreflect(method); result.asyncMethodName = method.getAnnotation(RemotableViewMethod.class).asyncImpl(); } catch (NoSuchMethodException | IllegalAccessException ex) { throw new ActionException("view: " + klass.getName() + " doesn't have method: " + methodName + getParameters(paramType)); } MethodKey key = new MethodKey(); key.set(klass, paramType, methodName); sMethods.put(key, result); } if (!async) { return result.syncMethod; } // Check this so see if async method is implemented or not. if (result.asyncMethodName.isEmpty()) { return null; } // Async method is lazily loaded. If it is not yet loaded, load now. if (result.asyncMethod == null) { MethodType asyncType = result.syncMethod.type() .dropParameterTypes(0, 1).changeReturnType(Runnable.class); try { result.asyncMethod = MethodHandles.publicLookup().findVirtual( klass, result.asyncMethodName, asyncType); } catch (NoSuchMethodException | IllegalAccessException ex) { throw new ActionException("Async implementation declared as " + result.asyncMethodName + " but not defined for " + methodName + ": public Runnable " + result.asyncMethodName + " (" + TextUtils.join(",", asyncType.parameterArray()) + ")"); } } return result.asyncMethod; } } private static String getParameters(Class paramType) { if (paramType == null) return "()"; return "(" + paramType + ")"; } /** * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, * on the {@link Drawable} of a given view. *

* The operation will be performed on the {@link Drawable} returned by the * target {@link View#getBackground()} by default. If targetBackground is false, * we assume the target is an {@link ImageView} and try applying the operations * to {@link ImageView#getDrawable()}. *

*/ private static class SetDrawableTint extends Action { boolean mTargetBackground; @ColorInt int mColorFilter; PorterDuff.Mode mFilterMode; SetDrawableTint(@IdRes int id, boolean targetBackground, @ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) { this.mViewId = id; this.mTargetBackground = targetBackground; this.mColorFilter = colorFilter; this.mFilterMode = mode; } SetDrawableTint(Parcel parcel) { mViewId = parcel.readInt(); mTargetBackground = parcel.readInt() != 0; mColorFilter = parcel.readInt(); mFilterMode = PorterDuff.intToMode(parcel.readInt()); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mTargetBackground ? 1 : 0); dest.writeInt(mColorFilter); dest.writeInt(PorterDuff.modeToInt(mFilterMode)); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; // Pick the correct drawable to modify for this view Drawable targetDrawable = null; if (mTargetBackground) { targetDrawable = target.getBackground(); } else if (target instanceof ImageView) { ImageView imageView = (ImageView) target; targetDrawable = imageView.getDrawable(); } if (targetDrawable != null) { targetDrawable.mutate().setColorFilter(mColorFilter, mFilterMode); } } @Override public int getActionTag() { return SET_DRAWABLE_TINT_TAG; } } /** * Equivalent to calling * {@link RippleDrawable#setColor(ColorStateList)}, * on the {@link Drawable} of a given view. *

* The operation will be performed on the {@link Drawable} returned by the * target {@link View#getBackground()}. *

*/ private class SetRippleDrawableColor extends Action { ColorStateList mColorStateList; SetRippleDrawableColor(@IdRes int id, ColorStateList colorStateList) { this.mViewId = id; this.mColorStateList = colorStateList; } SetRippleDrawableColor(Parcel parcel) { mViewId = parcel.readInt(); mColorStateList = parcel.readParcelable(null, android.content.res.ColorStateList.class); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeParcelable(mColorStateList, 0); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; // Pick the correct drawable to modify for this view Drawable targetDrawable = target.getBackground(); if (targetDrawable instanceof RippleDrawable) { ((RippleDrawable) targetDrawable.mutate()).setColor(mColorStateList); } } @Override public int getActionTag() { return SET_RIPPLE_DRAWABLE_COLOR_TAG; } } /** * @deprecated As RemoteViews may be reapplied frequently, it is preferable to call * {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change * unexpectedly. */ @Deprecated private final class ViewContentNavigation extends Action { final boolean mNext; ViewContentNavigation(@IdRes int viewId, boolean next) { this.mViewId = viewId; this.mNext = next; } ViewContentNavigation(Parcel in) { this.mViewId = in.readInt(); this.mNext = in.readBoolean(); } public void writeToParcel(Parcel out, int flags) { out.writeInt(this.mViewId); out.writeBoolean(this.mNext); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View view = root.findViewById(mViewId); if (view == null) return; try { getMethod(view, mNext ? "showNext" : "showPrevious", null, false /* async */).invoke(view); } catch (Throwable ex) { throw new ActionException(ex); } } public int mergeBehavior() { return MERGE_IGNORE; } @Override public int getActionTag() { return VIEW_CONTENT_NAVIGATION_TAG; } } private static class BitmapCache { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) ArrayList mBitmaps; SparseIntArray mBitmapHashes; int mBitmapMemory = -1; public BitmapCache() { mBitmaps = new ArrayList<>(); mBitmapHashes = new SparseIntArray(); } public BitmapCache(Parcel source) { mBitmaps = source.createTypedArrayList(Bitmap.CREATOR); mBitmapHashes = new SparseIntArray(); for (int i = 0; i < mBitmaps.size(); i++) { Bitmap b = mBitmaps.get(i); if (b != null) { mBitmapHashes.put(b.hashCode(), i); } } } public int getBitmapId(Bitmap b) { if (b == null) { return -1; } else { int hash = b.hashCode(); int hashId = mBitmapHashes.get(hash, -1); if (hashId != -1) { return hashId; } else { if (b.isMutable()) { b = b.asShared(); } mBitmaps.add(b); mBitmapHashes.put(hash, mBitmaps.size() - 1); mBitmapMemory = -1; return (mBitmaps.size() - 1); } } } @Nullable public Bitmap getBitmapForId(int id) { if (id == -1 || id >= mBitmaps.size()) { return null; } return mBitmaps.get(id); } public void writeBitmapsToParcel(Parcel dest, int flags) { dest.writeTypedList(mBitmaps, flags); } public int getBitmapMemory() { if (mBitmapMemory < 0) { mBitmapMemory = 0; int count = mBitmaps.size(); for (int i = 0; i < count; i++) { mBitmapMemory += mBitmaps.get(i).getAllocationByteCount(); } } return mBitmapMemory; } } private class BitmapReflectionAction extends Action { int mBitmapId; @UnsupportedAppUsage Bitmap mBitmap; @UnsupportedAppUsage String mMethodName; BitmapReflectionAction(@IdRes int viewId, String methodName, Bitmap bitmap) { this.mBitmap = bitmap; this.mViewId = viewId; this.mMethodName = methodName; mBitmapId = mBitmapCache.getBitmapId(bitmap); } BitmapReflectionAction(Parcel in) { mViewId = in.readInt(); mMethodName = in.readString8(); mBitmapId = in.readInt(); mBitmap = mBitmapCache.getBitmapForId(mBitmapId); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeString8(mMethodName); dest.writeInt(mBitmapId); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { ReflectionAction ra = new ReflectionAction(mViewId, mMethodName, BaseReflectionAction.BITMAP, mBitmap); ra.apply(root, rootParent, params); } @Override public void setHierarchyRootData(HierarchyRootData rootData) { mBitmapId = rootData.mBitmapCache.getBitmapId(mBitmap); } @Override public int getActionTag() { return BITMAP_REFLECTION_ACTION_TAG; } } /** * Base class for the reflection actions. */ private abstract static class BaseReflectionAction extends Action { static final int BOOLEAN = 1; static final int BYTE = 2; static final int SHORT = 3; static final int INT = 4; static final int LONG = 5; static final int FLOAT = 6; static final int DOUBLE = 7; static final int CHAR = 8; static final int STRING = 9; static final int CHAR_SEQUENCE = 10; static final int URI = 11; // BITMAP actions are never stored in the list of actions. They are only used locally // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache. static final int BITMAP = 12; static final int BUNDLE = 13; static final int INTENT = 14; static final int COLOR_STATE_LIST = 15; static final int ICON = 16; static final int BLEND_MODE = 17; @UnsupportedAppUsage String mMethodName; int mType; BaseReflectionAction(@IdRes int viewId, String methodName, int type) { this.mViewId = viewId; this.mMethodName = methodName; this.mType = type; } BaseReflectionAction(Parcel in) { this.mViewId = in.readInt(); this.mMethodName = in.readString8(); this.mType = in.readInt(); //noinspection ConstantIfStatement if (false) { Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.mViewId) + " methodName=" + this.mMethodName + " type=" + this.mType); } } public void writeToParcel(Parcel out, int flags) { out.writeInt(this.mViewId); out.writeString8(this.mMethodName); out.writeInt(this.mType); } /** * Returns the value to use as parameter for the method. * * The view might be passed as {@code null} if the parameter value is requested outside of * inflation. If the parameter cannot be determined at that time, the method should return * {@code null} but not raise any exception. */ @Nullable protected abstract Object getParameterValue(@Nullable View view) throws ActionException; @Override public final void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View view = root.findViewById(mViewId); if (view == null) return; Class param = getParameterType(this.mType); if (param == null) { throw new ActionException("bad type: " + this.mType); } Object value = getParameterValue(view); try { getMethod(view, this.mMethodName, param, false /* async */).invoke(view, value); } catch (Throwable ex) { throw new ActionException(ex); } } @Override public final Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { final View view = root.findViewById(mViewId); if (view == null) return ACTION_NOOP; Class param = getParameterType(this.mType); if (param == null) { throw new ActionException("bad type: " + this.mType); } Object value = getParameterValue(view); try { MethodHandle method = getMethod(view, this.mMethodName, param, true /* async */); // Upload the bitmap to GPU if the parameter is of type Bitmap or Icon. // Since bitmaps in framework are seldomly modified, this is supposed to accelerate // the operations. if (value instanceof Bitmap bitmap) { bitmap.prepareToDraw(); } if (value instanceof Icon icon && (icon.getType() == Icon.TYPE_BITMAP || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { Bitmap bitmap = icon.getBitmap(); if (bitmap != null) { bitmap.prepareToDraw(); } } if (method != null) { Runnable endAction = (Runnable) method.invoke(view, value); if (endAction == null) { return ACTION_NOOP; } // Special case view stub if (endAction instanceof ViewStub.ViewReplaceRunnable) { root.createTree(); // Replace child tree root.findViewTreeById(mViewId).replaceView( ((ViewStub.ViewReplaceRunnable) endAction).view); } return new RunnableAction(endAction); } } catch (Throwable ex) { throw new ActionException(ex); } return this; } public final int mergeBehavior() { // smoothScrollBy is cumulative, everything else overwites. if (mMethodName.equals("smoothScrollBy")) { return MERGE_APPEND; } else { return MERGE_REPLACE; } } @Override public final String getUniqueKey() { // Each type of reflection action corresponds to a setter, so each should be seen as // unique from the standpoint of merging. return super.getUniqueKey() + this.mMethodName + this.mType; } @Override public final boolean prefersAsyncApply() { return this.mType == URI || this.mType == ICON; } @Override public void visitUris(@NonNull Consumer visitor) { switch (this.mType) { case URI: final Uri uri = (Uri) getParameterValue(null); if (uri != null) visitor.accept(uri); break; case ICON: final Icon icon = (Icon) getParameterValue(null); if (icon != null) visitIconUri(icon, visitor); break; case INTENT: final Intent intent = (Intent) getParameterValue(null); if (intent != null) intent.visitUris(visitor); break; // TODO(b/281044385): Should we do anything about type BUNDLE? } } } /** Class for the reflection actions. */ private static final class ReflectionAction extends BaseReflectionAction { @UnsupportedAppUsage Object mValue; ReflectionAction(@IdRes int viewId, String methodName, int type, Object value) { super(viewId, methodName, type); this.mValue = value; } ReflectionAction(Parcel in) { super(in); // For some values that may have been null, we first check a flag to see if they were // written to the parcel. switch (this.mType) { case BOOLEAN: this.mValue = in.readBoolean(); break; case BYTE: this.mValue = in.readByte(); break; case SHORT: this.mValue = (short) in.readInt(); break; case INT: this.mValue = in.readInt(); break; case LONG: this.mValue = in.readLong(); break; case FLOAT: this.mValue = in.readFloat(); break; case DOUBLE: this.mValue = in.readDouble(); break; case CHAR: this.mValue = (char) in.readInt(); break; case STRING: this.mValue = in.readString8(); break; case CHAR_SEQUENCE: this.mValue = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); break; case URI: this.mValue = in.readTypedObject(Uri.CREATOR); break; case BITMAP: this.mValue = in.readTypedObject(Bitmap.CREATOR); break; case BUNDLE: // Because we use Parcel.allowSquashing() when writing, and that affects // how the contents of Bundles are written, we need to ensure the bundle is // unparceled immediately, not lazily. Setting a custom ReadWriteHelper // just happens to have that effect on Bundle.readFromParcel(). // TODO(b/212731590): build this state tracking into Bundle if (in.hasReadWriteHelper()) { this.mValue = in.readBundle(); } else { in.setReadWriteHelper(ALTERNATIVE_DEFAULT); this.mValue = in.readBundle(); in.setReadWriteHelper(null); } break; case INTENT: this.mValue = in.readTypedObject(Intent.CREATOR); break; case COLOR_STATE_LIST: this.mValue = in.readTypedObject(ColorStateList.CREATOR); break; case ICON: this.mValue = in.readTypedObject(Icon.CREATOR); break; case BLEND_MODE: this.mValue = BlendMode.fromValue(in.readInt()); break; default: break; } } public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); // For some values which are null, we record an integer flag to indicate whether // we have written a valid value to the parcel. switch (this.mType) { case BOOLEAN: out.writeBoolean((Boolean) this.mValue); break; case BYTE: out.writeByte((Byte) this.mValue); break; case SHORT: out.writeInt((Short) this.mValue); break; case INT: out.writeInt((Integer) this.mValue); break; case LONG: out.writeLong((Long) this.mValue); break; case FLOAT: out.writeFloat((Float) this.mValue); break; case DOUBLE: out.writeDouble((Double) this.mValue); break; case CHAR: out.writeInt((int) ((Character) this.mValue).charValue()); break; case STRING: out.writeString8((String) this.mValue); break; case CHAR_SEQUENCE: TextUtils.writeToParcel((CharSequence) this.mValue, out, flags); break; case BUNDLE: out.writeBundle((Bundle) this.mValue); break; case BLEND_MODE: out.writeInt(BlendMode.toValue((BlendMode) this.mValue)); break; case URI: case BITMAP: case INTENT: case COLOR_STATE_LIST: case ICON: out.writeTypedObject((Parcelable) this.mValue, flags); break; default: break; } } @Nullable @Override protected Object getParameterValue(@Nullable View view) throws ActionException { return this.mValue; } @Override public int getActionTag() { return REFLECTION_ACTION_TAG; } } private static final class ResourceReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; static final int STRING_RESOURCE = 3; private final int mResourceType; private final int mResId; ResourceReflectionAction(@IdRes int viewId, String methodName, int parameterType, int resourceType, int resId) { super(viewId, methodName, parameterType); this.mResourceType = resourceType; this.mResId = resId; } ResourceReflectionAction(Parcel in) { super(in); this.mResourceType = in.readInt(); this.mResId = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(this.mResourceType); dest.writeInt(this.mResId); } @Nullable @Override protected Object getParameterValue(@Nullable View view) throws ActionException { if (view == null) return null; Resources resources = view.getContext().getResources(); try { switch (this.mResourceType) { case DIMEN_RESOURCE: switch (this.mType) { case BaseReflectionAction.INT: return mResId == 0 ? 0 : resources.getDimensionPixelSize(mResId); case BaseReflectionAction.FLOAT: return mResId == 0 ? 0f : resources.getDimension(mResId); default: throw new ActionException( "dimen resources must be used as INT or FLOAT, " + "not " + this.mType); } case COLOR_RESOURCE: switch (this.mType) { case BaseReflectionAction.INT: return mResId == 0 ? 0 : view.getContext().getColor(mResId); case BaseReflectionAction.COLOR_STATE_LIST: return mResId == 0 ? null : view.getContext().getColorStateList(mResId); default: throw new ActionException( "color resources must be used as INT or COLOR_STATE_LIST," + " not " + this.mType); } case STRING_RESOURCE: switch (this.mType) { case BaseReflectionAction.CHAR_SEQUENCE: return mResId == 0 ? null : resources.getText(mResId); case BaseReflectionAction.STRING: return mResId == 0 ? null : resources.getString(mResId); default: throw new ActionException( "string resources must be used as STRING or CHAR_SEQUENCE," + " not " + this.mType); } default: throw new ActionException("unknown resource type: " + this.mResourceType); } } catch (ActionException ex) { throw ex; } catch (Throwable t) { throw new ActionException(t); } } @Override public int getActionTag() { return RESOURCE_REFLECTION_ACTION_TAG; } } private static final class AttributeReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; static final int STRING_RESOURCE = 3; private final int mResourceType; private final int mAttrId; AttributeReflectionAction(@IdRes int viewId, String methodName, int parameterType, int resourceType, int attrId) { super(viewId, methodName, parameterType); this.mResourceType = resourceType; this.mAttrId = attrId; } AttributeReflectionAction(Parcel in) { super(in); this.mResourceType = in.readInt(); this.mAttrId = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(this.mResourceType); dest.writeInt(this.mAttrId); } @Override protected Object getParameterValue(View view) throws ActionException { TypedArray typedArray = view.getContext().obtainStyledAttributes(new int[]{mAttrId}); try { // When mAttrId == 0, we will depend on the default values below if (mAttrId != 0 && typedArray.getType(0) == TypedValue.TYPE_NULL) { throw new ActionException("Attribute 0x" + Integer.toHexString(this.mAttrId) + " is not defined"); } switch (this.mResourceType) { case DIMEN_RESOURCE: switch (this.mType) { case BaseReflectionAction.INT: return typedArray.getDimensionPixelSize(0, 0); case BaseReflectionAction.FLOAT: return typedArray.getDimension(0, 0); default: throw new ActionException( "dimen attribute 0x" + Integer.toHexString(this.mAttrId) + " must be used as INT or FLOAT," + " not " + this.mType); } case COLOR_RESOURCE: switch (this.mType) { case BaseReflectionAction.INT: return typedArray.getColor(0, 0); case BaseReflectionAction.COLOR_STATE_LIST: return typedArray.getColorStateList(0); default: throw new ActionException( "color attribute 0x" + Integer.toHexString(this.mAttrId) + " must be used as INT or COLOR_STATE_LIST," + " not " + this.mType); } case STRING_RESOURCE: switch (this.mType) { case BaseReflectionAction.CHAR_SEQUENCE: return typedArray.getText(0); case BaseReflectionAction.STRING: return typedArray.getString(0); default: throw new ActionException( "string attribute 0x" + Integer.toHexString(this.mAttrId) + " must be used as STRING or CHAR_SEQUENCE," + " not " + this.mType); } default: // Note: This can only be an implementation error. throw new ActionException( "Unknown resource type: " + this.mResourceType); } } catch (ActionException ex) { throw ex; } catch (Throwable t) { throw new ActionException(t); } finally { typedArray.recycle(); } } @Override public int getActionTag() { return ATTRIBUTE_REFLECTION_ACTION_TAG; } } private static final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction { private final float mValue; @ComplexDimensionUnit private final int mUnit; ComplexUnitDimensionReflectionAction(int viewId, String methodName, int parameterType, float value, @ComplexDimensionUnit int unit) { super(viewId, methodName, parameterType); this.mValue = value; this.mUnit = unit; } ComplexUnitDimensionReflectionAction(Parcel in) { super(in); this.mValue = in.readFloat(); this.mUnit = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeFloat(this.mValue); dest.writeInt(this.mUnit); } @Nullable @Override protected Object getParameterValue(@Nullable View view) throws ActionException { if (view == null) return null; DisplayMetrics dm = view.getContext().getResources().getDisplayMetrics(); try { int data = TypedValue.createComplexDimension(this.mValue, this.mUnit); switch (this.mType) { case ReflectionAction.INT: return TypedValue.complexToDimensionPixelSize(data, dm); case ReflectionAction.FLOAT: return TypedValue.complexToDimension(data, dm); default: throw new ActionException( "parameter type must be INT or FLOAT, not " + this.mType); } } catch (ActionException ex) { throw ex; } catch (Throwable t) { throw new ActionException(t); } } @Override public int getActionTag() { return COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG; } } private static final class NightModeReflectionAction extends BaseReflectionAction { private final Object mLightValue; private final Object mDarkValue; NightModeReflectionAction( @IdRes int viewId, String methodName, int type, Object lightValue, Object darkValue) { super(viewId, methodName, type); mLightValue = lightValue; mDarkValue = darkValue; } NightModeReflectionAction(Parcel in) { super(in); switch (this.mType) { case ICON: mLightValue = in.readTypedObject(Icon.CREATOR); mDarkValue = in.readTypedObject(Icon.CREATOR); break; case COLOR_STATE_LIST: mLightValue = in.readTypedObject(ColorStateList.CREATOR); mDarkValue = in.readTypedObject(ColorStateList.CREATOR); break; case INT: mLightValue = in.readInt(); mDarkValue = in.readInt(); break; default: throw new ActionException("Unexpected night mode action type: " + this.mType); } } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); switch (this.mType) { case ICON: case COLOR_STATE_LIST: out.writeTypedObject((Parcelable) mLightValue, flags); out.writeTypedObject((Parcelable) mDarkValue, flags); break; case INT: out.writeInt((int) mLightValue); out.writeInt((int) mDarkValue); break; } } @Nullable @Override protected Object getParameterValue(@Nullable View view) throws ActionException { if (view == null) return null; Configuration configuration = view.getResources().getConfiguration(); return configuration.isNightModeActive() ? mDarkValue : mLightValue; } @Override public int getActionTag() { return NIGHT_MODE_REFLECTION_ACTION_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { if (this.mType == ICON) { visitIconUri((Icon) mDarkValue, visitor); visitIconUri((Icon) mLightValue, visitor); } } } /** * This is only used for async execution of actions and it not parcelable. */ private static final class RunnableAction extends RuntimeAction { private final Runnable mRunnable; RunnableAction(Runnable r) { mRunnable = r; } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { mRunnable.run(); } } private static boolean hasStableId(View view) { Object tag = view.getTag(com.android.internal.R.id.remote_views_stable_id); return tag != null; } private static int getStableId(View view) { Integer id = (Integer) view.getTag(com.android.internal.R.id.remote_views_stable_id); return id == null ? ViewGroupActionAdd.NO_ID : id; } private static void setStableId(View view, int stableId) { view.setTagInternal(com.android.internal.R.id.remote_views_stable_id, stableId); } // Returns the next recyclable child of the view group, or -1 if there are none. private static int getNextRecyclableChild(ViewGroup vg) { Integer tag = (Integer) vg.getTag(com.android.internal.R.id.remote_views_next_child); return tag == null ? -1 : tag; } private static int getViewLayoutId(View v) { return (Integer) v.getTag(R.id.widget_frame); } private static void setNextRecyclableChild(ViewGroup vg, int nextChild, int numChildren) { if (nextChild < 0 || nextChild >= numChildren) { vg.setTagInternal(com.android.internal.R.id.remote_views_next_child, -1); } else { vg.setTagInternal(com.android.internal.R.id.remote_views_next_child, nextChild); } } private void finalizeViewRecycling(ViewGroup root) { // Remove any recyclable children that were not used. nextChild should either be -1 or point // to the next recyclable child that hasn't been recycled. int nextChild = getNextRecyclableChild(root); if (nextChild >= 0 && nextChild < root.getChildCount()) { root.removeViews(nextChild, root.getChildCount() - nextChild); } // Make sure on the next round, we don't try to recycle if removeAllViews is not called. setNextRecyclableChild(root, -1, 0); // Traverse the view tree. for (int i = 0; i < root.getChildCount(); i++) { View child = root.getChildAt(i); if (child instanceof ViewGroup && !child.isRootNamespace()) { finalizeViewRecycling((ViewGroup) child); } } } /** * ViewGroup methods that are related to adding Views. */ private class ViewGroupActionAdd extends Action { static final int NO_ID = -1; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private RemoteViews mNestedViews; private int mIndex; private int mStableId; ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews) { this(viewId, nestedViews, -1 /* index */, NO_ID /* nestedViewId */); } ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews, int index) { this(viewId, nestedViews, index, NO_ID /* nestedViewId */); } ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews, int index, int stableId) { this.mViewId = viewId; mNestedViews = nestedViews; mIndex = index; mStableId = stableId; nestedViews.configureAsChild(getHierarchyRootData()); } ViewGroupActionAdd(Parcel parcel, ApplicationInfo info, int depth) { mViewId = parcel.readInt(); mIndex = parcel.readInt(); mStableId = parcel.readInt(); mNestedViews = new RemoteViews(parcel, getHierarchyRootData(), info, depth); mNestedViews.addFlags(mApplyFlags); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mIndex); dest.writeInt(mStableId); mNestedViews.writeToParcel(dest, flags); } @Override public void setHierarchyRootData(HierarchyRootData root) { mNestedViews.configureAsChild(root); } private int findViewIndexToRecycle(ViewGroup target, RemoteViews newContent) { for (int nextChild = getNextRecyclableChild(target); nextChild < target.getChildCount(); nextChild++) { View child = target.getChildAt(nextChild); if (getStableId(child) == mStableId) { return nextChild; } } return -1; } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final Context context = root.getContext(); final ViewGroup target = root.findViewById(mViewId); if (target == null) { return; } // If removeAllViews was called, this returns the next potential recycled view. // If there are no more views to recycle (or removeAllViews was not called), this // will return -1. final int nextChild = getNextRecyclableChild(target); RemoteViews rvToApply = mNestedViews.getRemoteViewsToApply(context); int flagsToPropagate = mApplyFlags & FLAG_MASK_TO_PROPAGATE; if (flagsToPropagate != 0) rvToApply.addFlags(flagsToPropagate); if (nextChild >= 0 && mStableId != NO_ID) { // At that point, the views starting at index nextChild are the ones recyclable but // not yet recycled. All views added on that round of application are placed before. // Find the next view with the same stable id, or -1. int recycledViewIndex = findViewIndexToRecycle(target, rvToApply); if (recycledViewIndex >= 0) { View child = target.getChildAt(recycledViewIndex); if (rvToApply.canRecycleView(child)) { if (nextChild < recycledViewIndex) { target.removeViews(nextChild, recycledViewIndex - nextChild); } setNextRecyclableChild(target, nextChild + 1, target.getChildCount()); rvToApply.reapplyNestedViews(context, child, rootParent, params); return; } // If we cannot recycle the views, we still remove all views in between to // avoid weird behaviors and insert the new view in place of the old one. target.removeViews(nextChild, recycledViewIndex - nextChild + 1); } } // If we cannot recycle, insert the new view before the next recyclable child. // Inflate nested views and add as children View nestedView = rvToApply.apply(context, target, rootParent, null /* size */, params); if (mStableId != NO_ID) { setStableId(nestedView, mStableId); } target.addView(nestedView, mIndex >= 0 ? mIndex : nextChild); if (nextChild >= 0) { // If we are at the end, there is no reason to try to recycle anymore setNextRecyclableChild(target, nextChild + 1, target.getChildCount()); } } @Override public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { // In the async implementation, update the view tree so that subsequent calls to // findViewById return the current view. root.createTree(); ViewTree target = root.findViewTreeById(mViewId); if ((target == null) || !(target.mRoot instanceof ViewGroup)) { return ACTION_NOOP; } final ViewGroup targetVg = (ViewGroup) target.mRoot; // Inflate nested views and perform all the async tasks for the child remoteView. final Context context = root.mRoot.getContext(); // If removeAllViews was called, this returns the next potential recycled view. // If there are no more views to recycle (or removeAllViews was not called), this // will return -1. final int nextChild = getNextRecyclableChild(targetVg); if (nextChild >= 0 && mStableId != NO_ID) { RemoteViews rvToApply = mNestedViews.getRemoteViewsToApply(context); final int recycledViewIndex = target.findChildIndex(nextChild, view -> getStableId(view) == mStableId); if (recycledViewIndex >= 0) { // At that point, the views starting at index nextChild are the ones // recyclable but not yet recycled. All views added on that round of // application are placed before. ViewTree recycled = target.mChildren.get(recycledViewIndex); // We can only recycle the view if the layout id is the same. if (rvToApply.canRecycleView(recycled.mRoot)) { if (recycledViewIndex > nextChild) { target.removeChildren(nextChild, recycledViewIndex - nextChild); } setNextRecyclableChild(targetVg, nextChild + 1, target.mChildren.size()); final AsyncApplyTask reapplyTask = rvToApply.getInternalAsyncApplyTask( context, targetVg, null /* listener */, params, null /* size */, recycled.mRoot); final ViewTree tree = reapplyTask.doInBackground(); if (tree == null) { throw new ActionException(reapplyTask.mError); } return new RuntimeAction() { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { reapplyTask.onPostExecute(tree); if (recycledViewIndex > nextChild) { targetVg.removeViews(nextChild, recycledViewIndex - nextChild); } } }; } // If the layout id is different, still remove the children as if we recycled // the view, to insert at the same place. target.removeChildren(nextChild, recycledViewIndex - nextChild + 1); return insertNewView(context, target, params, () -> targetVg.removeViews(nextChild, recycledViewIndex - nextChild + 1)); } } // If we cannot recycle, simply add the view at the same available slot. return insertNewView(context, target, params, () -> {}); } private Action insertNewView(Context context, ViewTree target, ActionApplyParams params, Runnable finalizeAction) { ViewGroup targetVg = (ViewGroup) target.mRoot; int nextChild = getNextRecyclableChild(targetVg); final AsyncApplyTask task = mNestedViews.getInternalAsyncApplyTask(context, targetVg, null /* listener */, params, null /* size */, null /* result */); final ViewTree tree = task.doInBackground(); if (tree == null) { throw new ActionException(task.mError); } if (mStableId != NO_ID) { setStableId(task.mResult, mStableId); } // Update the global view tree, so that next call to findViewTreeById // goes through the subtree as well. final int insertIndex = mIndex >= 0 ? mIndex : nextChild; target.addChild(tree, insertIndex); if (nextChild >= 0) { setNextRecyclableChild(targetVg, nextChild + 1, target.mChildren.size()); } return new RuntimeAction() { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { task.onPostExecute(tree); finalizeAction.run(); targetVg.addView(task.mResult, insertIndex); } }; } @Override public int mergeBehavior() { return MERGE_APPEND; } @Override public boolean prefersAsyncApply() { return mNestedViews.prefersAsyncApply(); } @Override public int getActionTag() { return VIEW_GROUP_ACTION_ADD_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { mNestedViews.visitUris(visitor); } } /** * ViewGroup methods related to removing child views. */ private static class ViewGroupActionRemove extends Action { /** * Id that indicates that all child views of the affected ViewGroup should be removed. * *

Using -2 because the default id is -1. This avoids accidentally matching that. */ private static final int REMOVE_ALL_VIEWS_ID = -2; private int mViewIdToKeep; ViewGroupActionRemove(@IdRes int viewId) { this(viewId, REMOVE_ALL_VIEWS_ID); } ViewGroupActionRemove(@IdRes int viewId, @IdRes int viewIdToKeep) { this.mViewId = viewId; mViewIdToKeep = viewIdToKeep; } ViewGroupActionRemove(Parcel parcel) { mViewId = parcel.readInt(); mViewIdToKeep = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mViewIdToKeep); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final ViewGroup target = root.findViewById(mViewId); if (target == null) { return; } if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) { // Remote any view without a stable id for (int i = target.getChildCount() - 1; i >= 0; i--) { if (!hasStableId(target.getChildAt(i))) { target.removeViewAt(i); } } // In the end, only children with a stable id (i.e. recyclable) are left. setNextRecyclableChild(target, 0, target.getChildCount()); return; } removeAllViewsExceptIdToKeep(target); } @Override public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { // In the async implementation, update the view tree so that subsequent calls to // findViewById return the current view. root.createTree(); ViewTree target = root.findViewTreeById(mViewId); if ((target == null) || !(target.mRoot instanceof ViewGroup)) { return ACTION_NOOP; } final ViewGroup targetVg = (ViewGroup) target.mRoot; if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) { target.mChildren.removeIf(childTree -> !hasStableId(childTree.mRoot)); setNextRecyclableChild(targetVg, 0, target.mChildren.size()); } else { // Remove just the children which don't match the excepted view target.mChildren.removeIf(childTree -> childTree.mRoot.getId() != mViewIdToKeep); if (target.mChildren.isEmpty()) { target.mChildren = null; } } return new RuntimeAction() { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) { for (int i = targetVg.getChildCount() - 1; i >= 0; i--) { if (!hasStableId(targetVg.getChildAt(i))) { targetVg.removeViewAt(i); } } return; } removeAllViewsExceptIdToKeep(targetVg); } }; } /** * Iterates through the children in the given ViewGroup and removes all the views that * do not have an id of {@link #mViewIdToKeep}. */ private void removeAllViewsExceptIdToKeep(ViewGroup viewGroup) { // Otherwise, remove all the views that do not match the id to keep. int index = viewGroup.getChildCount() - 1; while (index >= 0) { if (viewGroup.getChildAt(index).getId() != mViewIdToKeep) { viewGroup.removeViewAt(index); } index--; } } @Override public int getActionTag() { return VIEW_GROUP_ACTION_REMOVE_TAG; } @Override public int mergeBehavior() { return MERGE_APPEND; } } /** * Action to remove a view from its parent. */ private static class RemoveFromParentAction extends Action { RemoveFromParentAction(@IdRes int viewId) { this.mViewId = viewId; } RemoveFromParentAction(Parcel parcel) { mViewId = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null || target == root) { return; } ViewParent parent = target.getParent(); if (parent instanceof ViewManager) { ((ViewManager) parent).removeView(target); } } @Override public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { // In the async implementation, update the view tree so that subsequent calls to // findViewById return the correct view. root.createTree(); ViewTree target = root.findViewTreeById(mViewId); if (target == null || target == root) { return ACTION_NOOP; } ViewTree parent = root.findViewTreeParentOf(target); if (parent == null || !(parent.mRoot instanceof ViewManager)) { return ACTION_NOOP; } final ViewManager parentVg = (ViewManager) parent.mRoot; parent.mChildren.remove(target); return new RuntimeAction() { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { parentVg.removeView(target.mRoot); } }; } @Override public int getActionTag() { return REMOVE_FROM_PARENT_ACTION_TAG; } @Override public int mergeBehavior() { return MERGE_APPEND; } } /** * Helper action to set compound drawables on a TextView. Supports relative * (s/t/e/b) or cardinal (l/t/r/b) arrangement. */ private static class TextViewDrawableAction extends Action { boolean mIsRelative = false; boolean mUseIcons = false; int mD1, mD2, mD3, mD4; Icon mI1, mI2, mI3, mI4; boolean mDrawablesLoaded = false; Drawable mId1, mId2, mId3, mId4; public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, @DrawableRes int d1, @DrawableRes int d2, @DrawableRes int d3, @DrawableRes int d4) { this.mViewId = viewId; this.mIsRelative = isRelative; this.mUseIcons = false; this.mD1 = d1; this.mD2 = d2; this.mD3 = d3; this.mD4 = d4; } public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, Icon i1, Icon i2, Icon i3, Icon i4) { this.mViewId = viewId; this.mIsRelative = isRelative; this.mUseIcons = true; this.mI1 = i1; this.mI2 = i2; this.mI3 = i3; this.mI4 = i4; } public TextViewDrawableAction(Parcel parcel) { mViewId = parcel.readInt(); mIsRelative = (parcel.readInt() != 0); mUseIcons = (parcel.readInt() != 0); if (mUseIcons) { mI1 = parcel.readTypedObject(Icon.CREATOR); mI2 = parcel.readTypedObject(Icon.CREATOR); mI3 = parcel.readTypedObject(Icon.CREATOR); mI4 = parcel.readTypedObject(Icon.CREATOR); } else { mD1 = parcel.readInt(); mD2 = parcel.readInt(); mD3 = parcel.readInt(); mD4 = parcel.readInt(); } } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mIsRelative ? 1 : 0); dest.writeInt(mUseIcons ? 1 : 0); if (mUseIcons) { dest.writeTypedObject(mI1, 0); dest.writeTypedObject(mI2, 0); dest.writeTypedObject(mI3, 0); dest.writeTypedObject(mI4, 0); } else { dest.writeInt(mD1); dest.writeInt(mD2); dest.writeInt(mD3); dest.writeInt(mD4); } } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final TextView target = root.findViewById(mViewId); if (target == null) return; if (mDrawablesLoaded) { if (mIsRelative) { target.setCompoundDrawablesRelativeWithIntrinsicBounds(mId1, mId2, mId3, mId4); } else { target.setCompoundDrawablesWithIntrinsicBounds(mId1, mId2, mId3, mId4); } } else if (mUseIcons) { final Context ctx = target.getContext(); final Drawable id1 = mI1 == null ? null : mI1.loadDrawable(ctx); final Drawable id2 = mI2 == null ? null : mI2.loadDrawable(ctx); final Drawable id3 = mI3 == null ? null : mI3.loadDrawable(ctx); final Drawable id4 = mI4 == null ? null : mI4.loadDrawable(ctx); if (mIsRelative) { target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4); } else { target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4); } } else { if (mIsRelative) { target.setCompoundDrawablesRelativeWithIntrinsicBounds(mD1, mD2, mD3, mD4); } else { target.setCompoundDrawablesWithIntrinsicBounds(mD1, mD2, mD3, mD4); } } } @Override public Action initActionAsync(ViewTree root, ViewGroup rootParent, ActionApplyParams params) { final TextView target = root.findViewById(mViewId); if (target == null) return ACTION_NOOP; TextViewDrawableAction copy = mUseIcons ? new TextViewDrawableAction(mViewId, mIsRelative, mI1, mI2, mI3, mI4) : new TextViewDrawableAction(mViewId, mIsRelative, mD1, mD2, mD3, mD4); // Load the drawables on the background thread. copy.mDrawablesLoaded = true; final Context ctx = target.getContext(); if (mUseIcons) { copy.mId1 = mI1 == null ? null : mI1.loadDrawable(ctx); copy.mId2 = mI2 == null ? null : mI2.loadDrawable(ctx); copy.mId3 = mI3 == null ? null : mI3.loadDrawable(ctx); copy.mId4 = mI4 == null ? null : mI4.loadDrawable(ctx); } else { copy.mId1 = mD1 == 0 ? null : ctx.getDrawable(mD1); copy.mId2 = mD2 == 0 ? null : ctx.getDrawable(mD2); copy.mId3 = mD3 == 0 ? null : ctx.getDrawable(mD3); copy.mId4 = mD4 == 0 ? null : ctx.getDrawable(mD4); } return copy; } @Override public boolean prefersAsyncApply() { return mUseIcons; } @Override public int getActionTag() { return TEXT_VIEW_DRAWABLE_ACTION_TAG; } @Override public void visitUris(@NonNull Consumer visitor) { if (mUseIcons) { visitIconUri(mI1, visitor); visitIconUri(mI2, visitor); visitIconUri(mI3, visitor); visitIconUri(mI4, visitor); } } } /** * Helper action to set text size on a TextView in any supported units. */ private static class TextViewSizeAction extends Action { int mUnits; float mSize; TextViewSizeAction(@IdRes int viewId, @ComplexDimensionUnit int units, float size) { this.mViewId = viewId; this.mUnits = units; this.mSize = size; } TextViewSizeAction(Parcel parcel) { mViewId = parcel.readInt(); mUnits = parcel.readInt(); mSize = parcel.readFloat(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mUnits); dest.writeFloat(mSize); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final TextView target = root.findViewById(mViewId); if (target == null) return; target.setTextSize(mUnits, mSize); } @Override public int getActionTag() { return TEXT_VIEW_SIZE_ACTION_TAG; } } /** * Helper action to set padding on a View. */ private static class ViewPaddingAction extends Action { @Px int mLeft, mTop, mRight, mBottom; public ViewPaddingAction(@IdRes int viewId, @Px int left, @Px int top, @Px int right, @Px int bottom) { this.mViewId = viewId; this.mLeft = left; this.mTop = top; this.mRight = right; this.mBottom = bottom; } public ViewPaddingAction(Parcel parcel) { mViewId = parcel.readInt(); mLeft = parcel.readInt(); mTop = parcel.readInt(); mRight = parcel.readInt(); mBottom = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mLeft); dest.writeInt(mTop); dest.writeInt(mRight); dest.writeInt(mBottom); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; target.setPadding(mLeft, mTop, mRight, mBottom); } @Override public int getActionTag() { return VIEW_PADDING_ACTION_TAG; } } /** * Helper action to set layout params on a View. */ private static class LayoutParamAction extends Action { static final int LAYOUT_MARGIN_LEFT = MARGIN_LEFT; static final int LAYOUT_MARGIN_TOP = MARGIN_TOP; static final int LAYOUT_MARGIN_RIGHT = MARGIN_RIGHT; static final int LAYOUT_MARGIN_BOTTOM = MARGIN_BOTTOM; static final int LAYOUT_MARGIN_START = MARGIN_START; static final int LAYOUT_MARGIN_END = MARGIN_END; static final int LAYOUT_WIDTH = 8; static final int LAYOUT_HEIGHT = 9; final int mProperty; final int mValueType; final int mValue; /** * @param viewId ID of the view alter * @param property which layout parameter to alter * @param value new value of the layout parameter * @param units the units of the given value */ LayoutParamAction(@IdRes int viewId, int property, float value, @ComplexDimensionUnit int units) { this.mViewId = viewId; this.mProperty = property; this.mValueType = VALUE_TYPE_COMPLEX_UNIT; this.mValue = TypedValue.createComplexDimension(value, units); } /** * @param viewId ID of the view alter * @param property which layout parameter to alter * @param value value to set. * @param valueType must be one of {@link #VALUE_TYPE_COMPLEX_UNIT}, * {@link #VALUE_TYPE_RESOURCE}, {@link #VALUE_TYPE_ATTRIBUTE} or * {@link #VALUE_TYPE_RAW}. */ LayoutParamAction(@IdRes int viewId, int property, int value, @ValueType int valueType) { this.mViewId = viewId; this.mProperty = property; this.mValueType = valueType; this.mValue = value; } public LayoutParamAction(Parcel parcel) { mViewId = parcel.readInt(); mProperty = parcel.readInt(); mValueType = parcel.readInt(); mValue = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mProperty); dest.writeInt(mValueType); dest.writeInt(mValue); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) { return; } ViewGroup.LayoutParams layoutParams = target.getLayoutParams(); if (layoutParams == null) { return; } switch (mProperty) { case LAYOUT_MARGIN_LEFT: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).leftMargin = getPixelOffset(target); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_TOP: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).topMargin = getPixelOffset(target); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_RIGHT: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).rightMargin = getPixelOffset(target); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_BOTTOM: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).bottomMargin = getPixelOffset(target); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_START: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).setMarginStart(getPixelOffset(target)); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_END: if (layoutParams instanceof MarginLayoutParams) { ((MarginLayoutParams) layoutParams).setMarginEnd(getPixelOffset(target)); target.setLayoutParams(layoutParams); } break; case LAYOUT_WIDTH: layoutParams.width = getPixelSize(target); target.setLayoutParams(layoutParams); break; case LAYOUT_HEIGHT: layoutParams.height = getPixelSize(target); target.setLayoutParams(layoutParams); break; default: throw new IllegalArgumentException("Unknown property " + mProperty); } } private int getPixelOffset(View target) { try { switch (mValueType) { case VALUE_TYPE_ATTRIBUTE: TypedArray typedArray = target.getContext().obtainStyledAttributes( new int[]{this.mValue}); try { return typedArray.getDimensionPixelOffset(0, 0); } finally { typedArray.recycle(); } case VALUE_TYPE_RESOURCE: if (mValue == 0) { return 0; } return target.getResources().getDimensionPixelOffset(mValue); case VALUE_TYPE_COMPLEX_UNIT: return TypedValue.complexToDimensionPixelOffset(mValue, target.getResources().getDisplayMetrics()); default: return mValue; } } catch (Throwable t) { throw new ActionException(t); } } private int getPixelSize(View target) { try { switch (mValueType) { case VALUE_TYPE_ATTRIBUTE: TypedArray typedArray = target.getContext().obtainStyledAttributes( new int[]{this.mValue}); try { return typedArray.getDimensionPixelSize(0, 0); } finally { typedArray.recycle(); } case VALUE_TYPE_RESOURCE: if (mValue == 0) { return 0; } return target.getResources().getDimensionPixelSize(mValue); case VALUE_TYPE_COMPLEX_UNIT: return TypedValue.complexToDimensionPixelSize(mValue, target.getResources().getDisplayMetrics()); default: return mValue; } } catch (Throwable t) { throw new ActionException(t); } } @Override public int getActionTag() { return LAYOUT_PARAM_ACTION_TAG; } @Override public String getUniqueKey() { return super.getUniqueKey() + mProperty; } } /** * Helper action to add a view tag with RemoteInputs. */ private static class SetRemoteInputsAction extends Action { final Parcelable[] mRemoteInputs; public SetRemoteInputsAction(@IdRes int viewId, RemoteInput[] remoteInputs) { this.mViewId = viewId; this.mRemoteInputs = remoteInputs; } public SetRemoteInputsAction(Parcel parcel) { mViewId = parcel.readInt(); mRemoteInputs = parcel.createTypedArray(RemoteInput.CREATOR); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeTypedArray(mRemoteInputs, flags); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; target.setTagInternal(R.id.remote_input_tag, mRemoteInputs); } @Override public int getActionTag() { return SET_REMOTE_INPUTS_ACTION_TAG; } } private static class SetIntTagAction extends Action { @IdRes private final int mViewId; @IdRes private final int mKey; private final int mTag; SetIntTagAction(@IdRes int viewId, @IdRes int key, int tag) { mViewId = viewId; mKey = key; mTag = tag; } SetIntTagAction(Parcel parcel) { mViewId = parcel.readInt(); mKey = parcel.readInt(); mTag = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mKey); dest.writeInt(mTag); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { final View target = root.findViewById(mViewId); if (target == null) return; target.setTagInternal(mKey, mTag); } @Override public int getActionTag() { return SET_INT_TAG_TAG; } } private static class SetCompoundButtonCheckedAction extends Action { private final boolean mChecked; SetCompoundButtonCheckedAction(@IdRes int viewId, boolean checked) { this.mViewId = viewId; mChecked = checked; } SetCompoundButtonCheckedAction(Parcel in) { mViewId = in.readInt(); mChecked = in.readBoolean(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeBoolean(mChecked); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { final View target = root.findViewById(mViewId); if (target == null) return; if (!(target instanceof CompoundButton)) { Log.w(LOG_TAG, "Cannot set checked to view " + mViewId + " because it is not a CompoundButton"); return; } CompoundButton button = (CompoundButton) target; Object tag = button.getTag(R.id.remote_checked_change_listener_tag); // Temporarily unset the checked change listener so calling setChecked doesn't launch // the intent. if (tag instanceof OnCheckedChangeListener) { button.setOnCheckedChangeListener(null); button.setChecked(mChecked); button.setOnCheckedChangeListener((OnCheckedChangeListener) tag); } else { button.setChecked(mChecked); } } @Override public int getActionTag() { return SET_COMPOUND_BUTTON_CHECKED_TAG; } } private static class SetRadioGroupCheckedAction extends Action { @IdRes private final int mCheckedId; SetRadioGroupCheckedAction(@IdRes int viewId, @IdRes int checkedId) { this.mViewId = viewId; mCheckedId = checkedId; } SetRadioGroupCheckedAction(Parcel in) { mViewId = in.readInt(); mCheckedId = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mCheckedId); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { final View target = root.findViewById(mViewId); if (target == null) return; if (!(target instanceof RadioGroup)) { Log.w(LOG_TAG, "Cannot check " + mViewId + " because it's not a RadioGroup"); return; } RadioGroup group = (RadioGroup) target; // Temporarily unset all the checked change listeners while we check the group. for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); if (!(child instanceof CompoundButton)) continue; Object tag = child.getTag(R.id.remote_checked_change_listener_tag); if (!(tag instanceof OnCheckedChangeListener)) continue; // Clear the checked change listener, we'll restore it after the check. ((CompoundButton) child).setOnCheckedChangeListener(null); } group.check(mCheckedId); // Loop through the children again and restore the checked change listeners. for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); if (!(child instanceof CompoundButton)) continue; Object tag = child.getTag(R.id.remote_checked_change_listener_tag); if (!(tag instanceof OnCheckedChangeListener)) continue; ((CompoundButton) child).setOnCheckedChangeListener((OnCheckedChangeListener) tag); } } @Override public int getActionTag() { return SET_RADIO_GROUP_CHECKED; } } private static class SetViewOutlinePreferredRadiusAction extends Action { @ValueType private final int mValueType; private final int mValue; SetViewOutlinePreferredRadiusAction(@IdRes int viewId, int value, @ValueType int valueType) { this.mViewId = viewId; this.mValueType = valueType; this.mValue = value; } SetViewOutlinePreferredRadiusAction( @IdRes int viewId, float radius, @ComplexDimensionUnit int units) { this.mViewId = viewId; this.mValueType = VALUE_TYPE_COMPLEX_UNIT; this.mValue = TypedValue.createComplexDimension(radius, units); } SetViewOutlinePreferredRadiusAction(Parcel in) { mViewId = in.readInt(); mValueType = in.readInt(); mValue = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewId); dest.writeInt(mValueType); dest.writeInt(mValue); } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { final View target = root.findViewById(mViewId); if (target == null) return; try { float radius; switch (mValueType) { case VALUE_TYPE_ATTRIBUTE: TypedArray typedArray = target.getContext().obtainStyledAttributes( new int[]{mValue}); try { radius = typedArray.getDimension(0, 0); } finally { typedArray.recycle(); } break; case VALUE_TYPE_RESOURCE: radius = mValue == 0 ? 0 : target.getResources().getDimension(mValue); break; case VALUE_TYPE_COMPLEX_UNIT: radius = TypedValue.complexToDimension(mValue, target.getResources().getDisplayMetrics()); break; default: radius = mValue; } target.setOutlineProvider(new RemoteViewOutlineProvider(radius)); } catch (Throwable t) { throw new ActionException(t); } } @Override public int getActionTag() { return SET_VIEW_OUTLINE_RADIUS_TAG; } } /** * OutlineProvider for a view with a radius set by * {@link #setViewOutlinePreferredRadius(int, float, int)}. */ public static final class RemoteViewOutlineProvider extends ViewOutlineProvider { private final float mRadius; public RemoteViewOutlineProvider(float radius) { mRadius = radius; } /** Returns the corner radius used when providing the view outline. */ public float getRadius() { return mRadius; } @Override public void getOutline(@NonNull View view, @NonNull Outline outline) { outline.setRoundRect( 0 /*left*/, 0 /* top */, view.getWidth() /* right */, view.getHeight() /* bottom */, mRadius); } } private class SetDrawInstructionAction extends Action { @Nullable private final DrawInstructions mInstructions; SetDrawInstructionAction(@NonNull final DrawInstructions instructions) { mInstructions = instructions; } SetDrawInstructionAction(@NonNull final Parcel in) { if (drawDataParcel()) { mInstructions = DrawInstructions.readFromParcel(in); } else { mInstructions = null; } } @Override public void writeToParcel(Parcel dest, int flags) { if (drawDataParcel()) { DrawInstructions.writeToParcel(mInstructions, dest, flags); } } @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) throws ActionException { if (drawDataParcel() && mInstructions != null && root instanceof RemoteComposePlayer player) { final List bytes = mInstructions.mInstructions; if (bytes.isEmpty()) { return; } try (ByteArrayInputStream is = new ByteArrayInputStream(bytes.get(0))) { player.setDocument(new RemoteComposeDocument(is)); player.addClickListener((viewId, metadata) -> { mActions.forEach(action -> { if (viewId == action.mViewId && action instanceof SetOnClickResponse setOnClickResponse) { setOnClickResponse.mResponse.handleViewInteraction( player, params.handler); } }); }); } catch (IOException e) { Log.e(LOG_TAG, "Failed to render draw instructions", e); } } } @Override public int getActionTag() { return SET_DRAW_INSTRUCTION_TAG; } } /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. * * @param packageName Name of the package that contains the layout resource * @param layoutId The id of the layout resource */ public RemoteViews(String packageName, int layoutId) { this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId); } /** * Create a new RemoteViews object that will display the views contained * in the specified layout file and change the id of the root view to the specified one. * * @param packageName Name of the package that contains the layout resource * @param layoutId The id of the layout resource */ public RemoteViews(@NonNull String packageName, @LayoutRes int layoutId, @IdRes int viewId) { this(packageName, layoutId); this.mViewId = viewId; } /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. * * @param application The application whose content is shown by the views. * @param layoutId The id of the layout resource. * * @hide */ protected RemoteViews(ApplicationInfo application, @LayoutRes int layoutId) { mApplication = application; mLayoutId = layoutId; mApplicationInfoCache.put(application); } private boolean hasMultipleLayouts() { return hasLandscapeAndPortraitLayouts() || hasSizedRemoteViews(); } private boolean hasLandscapeAndPortraitLayouts() { return (mLandscape != null) && (mPortrait != null); } private boolean hasSizedRemoteViews() { return mSizedRemoteViews != null; } @Nullable private SizeF getIdealSize() { return mIdealSize; } private void setIdealSize(@Nullable SizeF 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 IllegalArgumentException("Both RemoteViews must be non-null"); } if (!landscape.hasSameAppInfo(portrait.mApplication)) { throw new IllegalArgumentException( "Both RemoteViews must share the same package and user"); } mApplication = portrait.mApplication; mLayoutId = portrait.mLayoutId; mViewId = portrait.mViewId; mLightBackgroundLayoutId = portrait.mLightBackgroundLayoutId; mLandscape = landscape; mPortrait = portrait; mClassCookies = (portrait.mClassCookies != null) ? portrait.mClassCookies : landscape.mClassCookies; configureDescendantsAsChildren(); } /** * 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 one with the smallest area. * * If the {@link RemoteViews} host provides the size of the view, the layout with the largest * area that fits entirely in the provided size will be used (i.e. the width and height of * the layout must be less than the size of the view, with a 1dp margin to account for * rounding). If no layout fits in the view, the layout with the smallest area will be used. * * @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 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) { // If the map only contains a single mapping, treat this as if that RemoteViews was // passed as the top-level RemoteViews. RemoteViews single = remoteViews.values().iterator().next(); initializeFrom(single, /* hierarchyRoot= */ single); return; } 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; mViewId = smallestView.mViewId; mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; configureDescendantsAsChildren(); } // Initialize mSizedRemoteViews and return the class cookies. private Map initializeSizedRemoteViews(Iterator remoteViews) { List sizedRemoteViews = new ArrayList<>(); Map classCookies = null; float viewArea = Float.MAX_VALUE; RemoteViews smallestView = null; while (remoteViews.hasNext()) { RemoteViews view = remoteViews.next(); SizeF size = view.getIdealSize(); if (size == null) { throw new IllegalStateException("Expected RemoteViews to have ideal size"); } float newViewArea = size.getWidth() * size.getHeight(); 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); } 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, /* hierarchyRoot= */ null); } /** * No-arg constructor for use with {@link #initializeFrom(RemoteViews, RemoteViews)}. A * constructor taking two RemoteViews parameters would clash with the landscape/portrait * constructor. */ private RemoteViews() {} private static RemoteViews createInitializedFrom(@NonNull RemoteViews src, @Nullable RemoteViews hierarchyRoot) { RemoteViews child = new RemoteViews(); child.initializeFrom(src, hierarchyRoot); return child; } private void initializeFrom(@NonNull RemoteViews src, @Nullable RemoteViews hierarchyRoot) { if (hierarchyRoot == null) { mBitmapCache = src.mBitmapCache; // We need to create a new instance because we don't reconstruct collection cache mCollectionCache = new RemoteCollectionCache(src.mCollectionCache); mApplicationInfoCache = src.mApplicationInfoCache; } else { mBitmapCache = hierarchyRoot.mBitmapCache; mCollectionCache = hierarchyRoot.mCollectionCache; mApplicationInfoCache = hierarchyRoot.mApplicationInfoCache; } if (hierarchyRoot == null || src.mIsRoot) { // If there's no provided root, or if src was itself a root, then this RemoteViews is // the root of the new hierarchy. mIsRoot = true; hierarchyRoot = this; } else { // Otherwise, we're a descendant in the hierarchy. mIsRoot = false; } mApplication = src.mApplication; mLayoutId = src.mLayoutId; mLightBackgroundLayoutId = src.mLightBackgroundLayoutId; mApplyFlags = src.mApplyFlags; mClassCookies = src.mClassCookies; mIdealSize = src.mIdealSize; mProviderInstanceId = src.mProviderInstanceId; mHasDrawInstructions = src.mHasDrawInstructions; if (src.hasLandscapeAndPortraitLayouts()) { mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot); mPortrait = createInitializedFrom(src.mPortrait, hierarchyRoot); } if (src.hasSizedRemoteViews()) { mSizedRemoteViews = new ArrayList<>(src.mSizedRemoteViews.size()); for (RemoteViews srcView : src.mSizedRemoteViews) { mSizedRemoteViews.add(createInitializedFrom(srcView, hierarchyRoot)); } } if (src.mActions != null) { Parcel p = Parcel.obtain(); p.putClassCookies(mClassCookies); src.writeActionsToParcel(p, /* flags= */ 0); p.setDataPosition(0); // Since src is already in memory, we do not care about stack overflow as it has // already been read once. readActionsFromParcel(p, 0); p.recycle(); } // Now that everything is initialized and duplicated, create new caches for this // RemoteViews and recursively set up all descendants. if (mIsRoot) { reconstructCaches(); } } /** * Reads a RemoteViews object from a parcel. * * @param parcel the parcel object */ public RemoteViews(Parcel parcel) { this(parcel, /* rootData= */ null, /* info= */ null, /* depth= */ 0); } /** * Instantiates a RemoteViews object using {@link DrawInstructions}, which serves as an * alternative to XML layout. {@link DrawInstructions} objects contains the instructions which * can be interpreted and rendered accordingly in the host process. * * @param drawInstructions The {@link DrawInstructions} object */ @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public RemoteViews(@NonNull final DrawInstructions drawInstructions) { Objects.requireNonNull(drawInstructions); mHasDrawInstructions = true; addAction(new SetDrawInstructionAction(drawInstructions)); } private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData, @Nullable ApplicationInfo info, int depth) { if (depth > MAX_NESTED_VIEWS && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) { throw new IllegalArgumentException("Too many nested views."); } depth++; int mode = parcel.readInt(); if (rootData == null) { // We only store a bitmap cache in the root of the RemoteViews. mBitmapCache = new BitmapCache(parcel); // Store the class cookies such that they are available when we clone this RemoteView. mClassCookies = parcel.copyClassCookies(); mCollectionCache = new RemoteCollectionCache(parcel); } else { configureAsChild(rootData); } if (mode == MODE_NORMAL) { mApplication = parcel.readTypedObject(ApplicationInfo.CREATOR); mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel); mLayoutId = parcel.readInt(); mViewId = 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 = new ArrayList<>(numViews); for (int i = 0; i < numViews; i++) { RemoteViews view = new RemoteViews(parcel, getHierarchyRootData(), info, depth); info = view.mApplication; remoteViews.add(view); } initializeSizedRemoteViews(remoteViews.iterator()); RemoteViews smallestView = findSmallestRemoteView(); mApplication = smallestView.mApplication; mLayoutId = smallestView.mLayoutId; mViewId = smallestView.mViewId; mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; } else { // MODE_HAS_LANDSCAPE_AND_PORTRAIT mLandscape = new RemoteViews(parcel, getHierarchyRootData(), info, depth); mPortrait = new RemoteViews(parcel, getHierarchyRootData(), mLandscape.mApplication, depth); mApplication = mPortrait.mApplication; mLayoutId = mPortrait.mLayoutId; mViewId = mPortrait.mViewId; mLightBackgroundLayoutId = mPortrait.mLightBackgroundLayoutId; } mApplyFlags = parcel.readInt(); mProviderInstanceId = parcel.readLong(); mHasDrawInstructions = parcel.readBoolean(); // Ensure that all descendants have their caches set up recursively. if (mIsRoot) { configureDescendantsAsChildren(); } } private void readActionsFromParcel(Parcel parcel, int depth) { int count = parcel.readInt(); if (count > 0) { mActions = new ArrayList<>(count); for (int i = 0; i < count; i++) { mActions.add(getActionFromParcel(parcel, depth)); } } } private Action getActionFromParcel(Parcel parcel, int depth) { int tag = parcel.readInt(); switch (tag) { case SET_ON_CLICK_RESPONSE_TAG: return new SetOnClickResponse(parcel); case SET_DRAWABLE_TINT_TAG: return new SetDrawableTint(parcel); case REFLECTION_ACTION_TAG: return new ReflectionAction(parcel); case VIEW_GROUP_ACTION_ADD_TAG: return new ViewGroupActionAdd(parcel, mApplication, depth); case VIEW_GROUP_ACTION_REMOVE_TAG: return new ViewGroupActionRemove(parcel); case VIEW_CONTENT_NAVIGATION_TAG: return new ViewContentNavigation(parcel); case SET_EMPTY_VIEW_ACTION_TAG: return new SetEmptyView(parcel); case SET_PENDING_INTENT_TEMPLATE_TAG: return new SetPendingIntentTemplate(parcel); case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG: return new SetRemoteViewsAdapterIntent(parcel); case TEXT_VIEW_DRAWABLE_ACTION_TAG: return new TextViewDrawableAction(parcel); case TEXT_VIEW_SIZE_ACTION_TAG: return new TextViewSizeAction(parcel); case VIEW_PADDING_ACTION_TAG: return new ViewPaddingAction(parcel); case BITMAP_REFLECTION_ACTION_TAG: return new BitmapReflectionAction(parcel); case SET_REMOTE_INPUTS_ACTION_TAG: return new SetRemoteInputsAction(parcel); case LAYOUT_PARAM_ACTION_TAG: return new LayoutParamAction(parcel); case SET_RIPPLE_DRAWABLE_COLOR_TAG: return new SetRippleDrawableColor(parcel); case SET_INT_TAG_TAG: return new SetIntTagAction(parcel); case REMOVE_FROM_PARENT_ACTION_TAG: return new RemoveFromParentAction(parcel); case RESOURCE_REFLECTION_ACTION_TAG: return new ResourceReflectionAction(parcel); case COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG: return new ComplexUnitDimensionReflectionAction(parcel); case SET_COMPOUND_BUTTON_CHECKED_TAG: return new SetCompoundButtonCheckedAction(parcel); case SET_RADIO_GROUP_CHECKED: return new SetRadioGroupCheckedAction(parcel); case SET_VIEW_OUTLINE_RADIUS_TAG: return new SetViewOutlinePreferredRadiusAction(parcel); case SET_ON_CHECKED_CHANGE_RESPONSE_TAG: return new SetOnCheckedChangeResponse(parcel); case NIGHT_MODE_REFLECTION_ACTION_TAG: return new NightModeReflectionAction(parcel); case SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG: return new SetRemoteCollectionItemListAdapterAction(parcel); case ATTRIBUTE_REFLECTION_ACTION_TAG: return new AttributeReflectionAction(parcel); case SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG: return new SetOnStylusHandwritingResponse(parcel); case SET_DRAW_INSTRUCTION_TAG: return new SetDrawInstructionAction(parcel); default: throw new ActionException("Tag " + tag + " not found"); } } /** * Returns a deep copy of the RemoteViews object. The RemoteView may not be * attached to another RemoteView -- it must be the root of a hierarchy. * * @deprecated use {@link #RemoteViews(RemoteViews)} instead. * @throws IllegalStateException if this is not the root of a RemoteView * hierarchy */ @Override @Deprecated public RemoteViews clone() { Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. " + "May only clone the root of a RemoteView hierarchy."); return new RemoteViews(this); } public String getPackage() { return (mApplication != null) ? mApplication.packageName : null; } /** * Returns the layout id of the root layout associated with this RemoteViews. In the case * that the RemoteViews has both a landscape and portrait root, this will return the layout * id associated with the portrait layout. * * @return the layout id. */ public int getLayoutId() { return hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT) && (mLightBackgroundLayoutId != 0) ? mLightBackgroundLayoutId : mLayoutId; } /** * Sets the root of the hierarchy and then recursively traverses the tree to update the root * and populate caches for all descendants. */ private void configureAsChild(@NonNull HierarchyRootData rootData) { mIsRoot = false; mBitmapCache = rootData.mBitmapCache; mCollectionCache = rootData.mRemoteCollectionCache; mApplicationInfoCache = rootData.mApplicationInfoCache; mClassCookies = rootData.mClassCookies; configureDescendantsAsChildren(); } /** * Recursively traverses the tree to update the root and populate caches for all descendants. */ private void configureDescendantsAsChildren() { // Before propagating down the tree, replace our application from the root application info // cache, to ensure the same instance is present throughout the hierarchy to allow for // squashing. mApplication = mApplicationInfoCache.getOrPut(mApplication); HierarchyRootData rootData = getHierarchyRootData(); if (hasSizedRemoteViews()) { for (RemoteViews remoteView : mSizedRemoteViews) { remoteView.configureAsChild(rootData); } } else if (hasLandscapeAndPortraitLayouts()) { mLandscape.configureAsChild(rootData); mPortrait.configureAsChild(rootData); } else { if (mActions != null) { for (Action action : mActions) { action.setHierarchyRootData(rootData); } } } } /** * Recreates caches at the root level of the hierarchy, then recursively populates the caches * down the hierarchy. */ private void reconstructCaches() { if (!mIsRoot) return; mBitmapCache = new BitmapCache(); mApplicationInfoCache = new ApplicationInfoCache(); mApplication = mApplicationInfoCache.getOrPut(mApplication); configureDescendantsAsChildren(); } /** * Returns an estimate of the bitmap heap memory usage for this RemoteViews. */ /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public int estimateMemoryUsage() { return mBitmapCache.getBitmapMemory(); } /** * Add an action to be executed on the remote side when apply is called. * * @param a The action to add */ private void addAction(Action a) { 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<>(); } mActions.add(a); } /** * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the * given {@link RemoteViews}. This allows users to build "nested" * {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may * recycle layouts, use {@link #removeAllViews(int)} to clear any existing * children. * * @param viewId The id of the parent {@link ViewGroup} to add child into. * @param nestedView {@link RemoteViews} that describes the child. */ public void addView(@IdRes int viewId, RemoteViews nestedView) { // Clear all children when nested views omitted addAction(nestedView == null ? new ViewGroupActionRemove(viewId) : new ViewGroupActionAdd(viewId, nestedView)); } /** * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the given * {@link RemoteViews}. If the {@link RemoteViews} may be re-inflated or updated, * {@link #removeAllViews(int)} must be called on the same {@code viewId * } before the first call to this method for the behavior of this method to be predictable. * * The {@code stableId} will be used to identify a potential view to recycled when the remote * view is inflated. Views can be re-used if inserted in the same order, potentially with * some views appearing / disappearing. To be recycled the view must not change the layout * used to inflate it or its view id (see {@link RemoteViews#RemoteViews(String, int, int)}). * * Note: if a view is re-used, all the actions will be re-applied on it. However, its properties * are not reset, so what was applied in previous round will have an effect. As a view may be * re-created at any time by the host, the RemoteViews should not rely on keeping information * from previous applications and always re-set all the properties they need. * * @param viewId The id of the parent {@link ViewGroup} to add child into. * @param nestedView {@link RemoteViews} that describes the child. * @param stableId An id that is stable across different versions of RemoteViews. */ public void addStableView(@IdRes int viewId, @NonNull RemoteViews nestedView, int stableId) { addAction(new ViewGroupActionAdd(viewId, nestedView, -1 /* index */, stableId)); } /** * Equivalent to calling {@link ViewGroup#addView(View, int)} after inflating the * given {@link RemoteViews}. * * @param viewId The id of the parent {@link ViewGroup} to add the child into. * @param nestedView {@link RemoteViews} of the child to add. * @param index The position at which to add the child. * * @hide */ @UnsupportedAppUsage public void addView(@IdRes int viewId, RemoteViews nestedView, int index) { addAction(new ViewGroupActionAdd(viewId, nestedView, index)); } /** * Equivalent to calling {@link ViewGroup#removeAllViews()}. * * @param viewId The id of the parent {@link ViewGroup} to remove all * children from. */ public void removeAllViews(@IdRes int viewId) { addAction(new ViewGroupActionRemove(viewId)); } /** * Removes all views in the {@link ViewGroup} specified by the {@code viewId} except for any * child that has the {@code viewIdToKeep} as its id. * * @param viewId The id of the parent {@link ViewGroup} to remove children from. * @param viewIdToKeep The id of a child that should not be removed. * * @hide */ public void removeAllViewsExceptId(@IdRes int viewId, @IdRes int viewIdToKeep) { addAction(new ViewGroupActionRemove(viewId, viewIdToKeep)); } /** * Removes the {@link View} specified by the {@code viewId} from its parent {@link ViewManager}. * This will do nothing if the viewId specifies the root view of this RemoteViews. * * @param viewId The id of the {@link View} to remove from its parent. * * @hide */ public void removeFromParent(@IdRes int viewId) { addAction(new RemoveFromParentAction(viewId)); } /** * Equivalent to calling {@link AdapterViewAnimator#showNext()} * * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()} * @deprecated As RemoteViews may be reapplied frequently, it is preferable to call * {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change * unexpectedly. */ @Deprecated public void showNext(@IdRes int viewId) { addAction(new ViewContentNavigation(viewId, true /* next */)); } /** * Equivalent to calling {@link AdapterViewAnimator#showPrevious()} * * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()} * @deprecated As RemoteViews may be reapplied frequently, it is preferable to call * {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change * unexpectedly. */ @Deprecated public void showPrevious(@IdRes int viewId) { addAction(new ViewContentNavigation(viewId, false /* next */)); } /** * Equivalent to calling {@link AdapterViewAnimator#setDisplayedChild(int)} * * @param viewId The id of the view on which to call * {@link AdapterViewAnimator#setDisplayedChild(int)} */ public void setDisplayedChild(@IdRes int viewId, int childIndex) { setInt(viewId, "setDisplayedChild", childIndex); } /** * Equivalent to calling {@link View#setVisibility(int)} * * @param viewId The id of the view whose visibility should change * @param visibility The new visibility for the view */ public void setViewVisibility(@IdRes int viewId, @View.Visibility int visibility) { setInt(viewId, "setVisibility", visibility); } /** * Equivalent to calling {@link TextView#setText(CharSequence)} * * @param viewId The id of the view whose text should change * @param text The new text for the view */ public void setTextViewText(@IdRes int viewId, CharSequence text) { setCharSequence(viewId, "setText", text); } /** * Equivalent to calling {@link TextView#setTextSize(int, float)} * * @param viewId The id of the view whose text size should change * @param units The units of size (e.g. COMPLEX_UNIT_SP) * @param size The size of the text */ public void setTextViewTextSize(@IdRes int viewId, int units, float size) { addAction(new TextViewSizeAction(viewId, units, size)); } /** * Equivalent to calling * {@link TextView#setCompoundDrawablesWithIntrinsicBounds(int, int, int, int)}. * * @param viewId The id of the view whose text should change * @param left The id of a drawable to place to the left of the text, or 0 * @param top The id of a drawable to place above the text, or 0 * @param right The id of a drawable to place to the right of the text, or 0 * @param bottom The id of a drawable to place below the text, or 0 */ public void setTextViewCompoundDrawables(@IdRes int viewId, @DrawableRes int left, @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) { addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom)); } /** * Equivalent to calling {@link * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, int)}. * * @param viewId The id of the view whose text should change * @param start The id of a drawable to place before the text (relative to the * layout direction), or 0 * @param top The id of a drawable to place above the text, or 0 * @param end The id of a drawable to place after the text, or 0 * @param bottom The id of a drawable to place below the text, or 0 */ public void setTextViewCompoundDrawablesRelative(@IdRes int viewId, @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) { addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom)); } /** * Equivalent to calling {@link * TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)} * using the drawables yielded by {@link Icon#loadDrawable(Context)}. * * @param viewId The id of the view whose text should change * @param left an Icon to place to the left of the text, or 0 * @param top an Icon to place above the text, or 0 * @param right an Icon to place to the right of the text, or 0 * @param bottom an Icon to place below the text, or 0 * * @hide */ public void setTextViewCompoundDrawables(@IdRes int viewId, Icon left, Icon top, Icon right, Icon bottom) { addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom)); } /** * Equivalent to calling {@link * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)} * using the drawables yielded by {@link Icon#loadDrawable(Context)}. * * @param viewId The id of the view whose text should change * @param start an Icon to place before the text (relative to the * layout direction), or 0 * @param top an Icon to place above the text, or 0 * @param end an Icon to place after the text, or 0 * @param bottom an Icon to place below the text, or 0 * * @hide */ public void setTextViewCompoundDrawablesRelative(@IdRes int viewId, Icon start, Icon top, Icon end, Icon bottom) { addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom)); } /** * Equivalent to calling {@link ImageView#setImageResource(int)} * * @param viewId The id of the view whose drawable should change * @param srcId The new resource id for the drawable */ public void setImageViewResource(@IdRes int viewId, @DrawableRes int srcId) { setInt(viewId, "setImageResource", srcId); } /** * Equivalent to calling {@link ImageView#setImageURI(Uri)} * * @param viewId The id of the view whose drawable should change * @param uri The Uri for the image */ public void setImageViewUri(@IdRes int viewId, Uri uri) { setUri(viewId, "setImageURI", uri); } /** * Equivalent to calling {@link ImageView#setImageBitmap(Bitmap)} * * @param viewId The id of the view whose bitmap should change * @param bitmap The new Bitmap for the drawable */ public void setImageViewBitmap(@IdRes int viewId, Bitmap bitmap) { setBitmap(viewId, "setImageBitmap", bitmap); } /** * Equivalent to calling {@link ImageView#setImageIcon(Icon)} * * @param viewId The id of the view whose bitmap should change * @param icon The new Icon for the ImageView */ public void setImageViewIcon(@IdRes int viewId, Icon icon) { setIcon(viewId, "setImageIcon", icon); } /** * Equivalent to calling {@link AdapterView#setEmptyView(View)} * * @param viewId The id of the view on which to set the empty view * @param emptyViewId The view id of the empty view */ public void setEmptyView(@IdRes int viewId, @IdRes int emptyViewId) { addAction(new SetEmptyView(viewId, emptyViewId)); } /** * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase}, * {@link Chronometer#setFormat Chronometer.setFormat}, * and {@link Chronometer#start Chronometer.start()} or * {@link Chronometer#stop Chronometer.stop()}. * * @param viewId The id of the {@link Chronometer} to change * @param base The time at which the timer would have read 0:00. This * time should be based off of * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}. * @param format The Chronometer format string, or null to * simply display the timer value. * @param started True if you want the clock to be started, false if not. * * @see #setChronometerCountDown(int, boolean) */ public void setChronometer(@IdRes int viewId, long base, String format, boolean started) { setLong(viewId, "setBase", base); setString(viewId, "setFormat", format); setBoolean(viewId, "setStarted", started); } /** * Equivalent to calling {@link Chronometer#setCountDown(boolean) Chronometer.setCountDown} on * the chronometer with the given viewId. * * @param viewId The id of the {@link Chronometer} to change * @param isCountDown True if you want the chronometer to count down to base instead of * counting up. */ public void setChronometerCountDown(@IdRes int viewId, boolean isCountDown) { setBoolean(viewId, "setCountDown", isCountDown); } /** * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax}, * {@link ProgressBar#setProgress ProgressBar.setProgress}, and * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate} * * If indeterminate is true, then the values for max and progress are ignored. * * @param viewId The id of the {@link ProgressBar} to change * @param max The 100% value for the progress bar * @param progress The current value of the progress bar. * @param indeterminate True if the progress bar is indeterminate, * false if not. */ public void setProgressBar(@IdRes int viewId, int max, int progress, boolean indeterminate) { setBoolean(viewId, "setIndeterminate", indeterminate); if (!indeterminate) { setInt(viewId, "setMax", max); setInt(viewId, "setProgress", progress); } } /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} * to launch the provided {@link PendingIntent}. The source bounds * ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the clicked * view in screen space. * Note that any activity options associated with the mPendingIntent may get overridden * before starting the intent. * * When setting the on-click action of items within collections (eg. {@link ListView}, * {@link StackView} etc.), this method will not work. Instead, use {@link * RemoteViews#setPendingIntentTemplate(int, PendingIntent)} in conjunction with * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}. * * @param viewId The id of the view that will trigger the {@link PendingIntent} when clicked * @param pendingIntent The {@link PendingIntent} to send when user clicks */ public void setOnClickPendingIntent(@IdRes int viewId, PendingIntent pendingIntent) { setOnClickResponse(viewId, RemoteResponse.fromPendingIntent(pendingIntent)); } /** * Equivalent of calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} * to launch the provided {@link RemoteResponse}. * * @param viewId The id of the view that will trigger the {@link RemoteResponse} when clicked * @param response The {@link RemoteResponse} to send when user clicks */ public void setOnClickResponse(@IdRes int viewId, @NonNull RemoteResponse response) { addAction(new SetOnClickResponse(viewId, response)); } /** * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very * costly to set PendingIntents on the individual items, and is hence not recommended. Instead * this method should be used to set a single PendingIntent template on the collection, and * individual items can differentiate their on-click behavior using * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}. * * @param viewId The id of the collection who's children will use this PendingIntent template * when clicked * @param pendingIntentTemplate The {@link PendingIntent} to be combined with extras specified * by a child of viewId and executed when that child is clicked */ public void setPendingIntentTemplate(@IdRes int viewId, PendingIntent pendingIntentTemplate) { if (hasDrawInstructions()) { getPendingIntentTemplate().set(viewId, pendingIntentTemplate); tryAddRemoteResponse(viewId); } else { addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate)); } } /** * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very * costly to set PendingIntents on the individual items, and is hence not recommended. Instead * a single PendingIntent template can be set on the collection, see {@link * RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click * action of a given item can be distinguished by setting a fillInIntent on that item. The * fillInIntent is then combined with the PendingIntent template in order to determine the final * intent which will be executed when the item is clicked. This works as follows: any fields * which are left blank in the PendingIntent template, but are provided by the fillInIntent * will be overwritten, and the resulting PendingIntent will be used. The rest * of the PendingIntent template will then be filled in with the associated fields that are * set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details. * * @param viewId The id of the view on which to set the fillInIntent * @param fillInIntent The intent which will be combined with the parent's PendingIntent * in order to determine the on-click behavior of the view specified by viewId */ public void setOnClickFillInIntent(@IdRes int viewId, Intent fillInIntent) { if (hasDrawInstructions()) { getFillInIntent().set(viewId, fillInIntent); tryAddRemoteResponse(viewId); } else { setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent)); } } /** * Equivalent to calling * {@link android.widget.CompoundButton#setOnCheckedChangeListener( * android.widget.CompoundButton.OnCheckedChangeListener)} * to launch the provided {@link RemoteResponse}. * * The intent will be filled with the current checked state of the view at the key * {@link #EXTRA_CHECKED}. * * The {@link RemoteResponse} will not be launched in response to check changes arising from * {@link #setCompoundButtonChecked(int, boolean)} or {@link #setRadioGroupChecked(int, int)} * usages. * * The {@link RemoteResponse} must be created using * {@link RemoteResponse#fromFillInIntent(Intent)} in conjunction with * {@link RemoteViews#setPendingIntentTemplate(int, PendingIntent)} for items inside * collections (eg. {@link ListView}, {@link StackView} etc.). * * Otherwise, create the {@link RemoteResponse} using * {@link RemoteResponse#fromPendingIntent(PendingIntent)}. * * @param viewId The id of the view that will trigger the {@link PendingIntent} when checked * state changes. * @param response The {@link RemoteResponse} to send when the checked state changes. */ public void setOnCheckedChangeResponse( @IdRes int viewId, @NonNull RemoteResponse response) { addAction( new SetOnCheckedChangeResponse( viewId, response.setInteractionType( RemoteResponse.INTERACTION_TYPE_CHECKED_CHANGE))); } /** * Equivalent to calling {@link View#setHandwritingDelegatorCallback(Runnable)} to send the * provided {@link PendingIntent}. * *

A common use case is a remote view which looks like a text editor but does not actually * support text editing itself, and clicking on the remote view launches an activity containing * an EditText. To support handwriting initiation in this case, this method can be called on the * remote view to configure it as a handwriting delegator, meaning that stylus movement on the * remote view triggers a {@link PendingIntent} and starts handwriting mode for the delegate * EditText. The {@link PendingIntent} is typically the same as the one passed to {@link * #setOnClickPendingIntent} which launches the activity containing the EditText. The EditText * should call {@link View#setIsHandwritingDelegate} to set it as a delegate, and also use * {@link View#setAllowedHandwritingDelegatorPackage} or {@link * android.view.inputmethod.InputMethodManager#HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} * if necessary to support delegators from the package displaying the remote view. * * @param viewId identifier of the view that will trigger the {@link PendingIntent} when a * stylus {@link MotionEvent} occurs within the view's bounds * @param pendingIntent the {@link PendingIntent} to send, or {@code null} to clear the * handwriting delegation */ @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public void setOnStylusHandwritingPendingIntent( @IdRes int viewId, @Nullable PendingIntent pendingIntent) { addAction(new SetOnStylusHandwritingResponse(viewId, pendingIntent)); } /** * @hide * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, * on the {@link Drawable} of a given view. *

* * @param viewId The id of the view that contains the target * {@link Drawable} * @param targetBackground If true, apply these parameters to the * {@link Drawable} returned by * {@link android.view.View#getBackground()}. Otherwise, assume * the target view is an {@link ImageView} and apply them to * {@link ImageView#getDrawable()}. * @param colorFilter Specify a color for a * {@link android.graphics.ColorFilter} for this drawable. This will be ignored if * {@code mode} is {@code null}. * @param mode Specify a PorterDuff mode for this drawable, or null to leave * unchanged. */ public void setDrawableTint(@IdRes int viewId, boolean targetBackground, @ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) { addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode)); } /** * @hide * Equivalent to calling * {@link RippleDrawable#setColor(ColorStateList)} on the {@link Drawable} of a given view, * assuming it's a {@link RippleDrawable}. *

* * @param viewId The id of the view that contains the target * {@link RippleDrawable} * @param colorStateList Specify a color for a * {@link ColorStateList} for this drawable. */ public void setRippleDrawableColor(@IdRes int viewId, ColorStateList colorStateList) { addAction(new SetRippleDrawableColor(viewId, colorStateList)); } /** * @hide * Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}. * * @param viewId The id of the view whose tint should change * @param tint the tint to apply, may be {@code null} to clear tint */ public void setProgressTintList(@IdRes int viewId, ColorStateList tint) { addAction(new ReflectionAction(viewId, "setProgressTintList", BaseReflectionAction.COLOR_STATE_LIST, tint)); } /** * @hide * Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}. * * @param viewId The id of the view whose tint should change * @param tint the tint to apply, may be {@code null} to clear tint */ public void setProgressBackgroundTintList(@IdRes int viewId, ColorStateList tint) { addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList", BaseReflectionAction.COLOR_STATE_LIST, tint)); } /** * @hide * Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}. * * @param viewId The id of the view whose tint should change * @param tint the tint to apply, may be {@code null} to clear tint */ public void setProgressIndeterminateTintList(@IdRes int viewId, ColorStateList tint) { addAction(new ReflectionAction(viewId, "setIndeterminateTintList", BaseReflectionAction.COLOR_STATE_LIST, tint)); } /** * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}. * * @param viewId The id of the view whose text color should change * @param color Sets the text color for all the states (normal, selected, * focused) to be this color. */ public void setTextColor(@IdRes int viewId, @ColorInt int color) { setInt(viewId, "setTextColor", color); } /** * @hide * Equivalent to calling {@link android.widget.TextView#setTextColor(ColorStateList)}. * * @param viewId The id of the view whose text color should change * @param colors the text colors to set */ public void setTextColor(@IdRes int viewId, ColorStateList colors) { addAction(new ReflectionAction(viewId, "setTextColor", BaseReflectionAction.COLOR_STATE_LIST, colors)); } /** * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * * @param appWidgetId The id of the app widget which contains the specified view. (This * parameter is ignored in this deprecated method) * @param viewId The id of the {@link AdapterView} * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter * @deprecated This method has been deprecated. See * {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)} */ @Deprecated public void setRemoteAdapter(int appWidgetId, @IdRes int viewId, Intent intent) { setRemoteAdapter(viewId, intent); } /** * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * Can only be used for App Widgets. * * @param viewId The id of the {@link AdapterView} * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter * @deprecated use * {@link #setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} instead */ @Deprecated public void setRemoteAdapter(@IdRes int viewId, Intent intent) { if (remoteAdapterConversion()) { addAction(new SetRemoteCollectionItemListAdapterAction(viewId, intent)); } else { addAction(new SetRemoteViewsAdapterIntent(viewId, intent)); } } /** * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView, * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}. * This is a simpler but less flexible approach to populating collection widgets. Its use is * encouraged for most scenarios, as long as the total memory within the list of RemoteViews * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}. * * This API is supported in the compatibility library for previous API levels, see * RemoteViewsCompat. * * @param viewId The id of the {@link AdapterView} * @param list The list of RemoteViews which will populate the view specified by viewId. * @param viewTypeCount The maximum number of unique layout id's used to construct the list of * RemoteViews. This count cannot change during the life-cycle of a given widget, so this * parameter should account for the maximum possible number of types that may appear in the * See {@link Adapter#getViewTypeCount()}. * * @hide * @deprecated this appears to have no users outside of UnsupportedAppUsage? */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @Deprecated public void setRemoteAdapter(@IdRes int viewId, ArrayList list, int viewTypeCount) { RemoteCollectionItems.Builder b = new RemoteCollectionItems.Builder(); for (int i = 0; i < list.size(); i++) { b.addItem(i, list.get(i)); } setRemoteAdapter(viewId, b.setViewTypeCount(viewTypeCount).build()); } /** * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView, * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}. * This is a simpler but less flexible approach to populating collection widgets. Its use is * encouraged for most scenarios, as long as the total memory within the list of RemoteViews * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}. * * This API is supported in the compatibility library for previous API levels, see * RemoteViewsCompat. * * @param viewId The id of the {@link AdapterView}. * @param items The items to display in the {@link AdapterView}. */ public void setRemoteAdapter(@IdRes int viewId, @NonNull RemoteCollectionItems items) { addAction(new SetRemoteCollectionItemListAdapterAction(viewId, items)); } /** * Equivalent to calling {@link ListView#smoothScrollToPosition(int)}. * * @param viewId The id of the view to change * @param position Scroll to this adapter position */ public void setScrollPosition(@IdRes int viewId, int position) { setInt(viewId, "smoothScrollToPosition", position); } /** * Equivalent to calling {@link ListView#smoothScrollByOffset(int)}. * * @param viewId The id of the view to change * @param offset Scroll by this adapter position offset */ public void setRelativeScrollPosition(@IdRes int viewId, int offset) { setInt(viewId, "smoothScrollByOffset", offset); } /** * Equivalent to calling {@link android.view.View#setPadding(int, int, int, int)}. * * @param viewId The id of the view to change * @param left the left padding in pixels * @param top the top padding in pixels * @param right the right padding in pixels * @param bottom the bottom padding in pixels */ public void setViewPadding(@IdRes int viewId, @Px int left, @Px int top, @Px int right, @Px int bottom) { addAction(new ViewPaddingAction(viewId, left, top, right, bottom)); } /** * Equivalent to calling {@link MarginLayoutParams#setMarginEnd}. * Only works if the {@link View#getLayoutParams()} supports margins. * * @param viewId The id of the view to change * @param type The margin being set e.g. {@link #MARGIN_END} * @param dimen a dimension resource to apply to the margin, or 0 to clear the margin. */ public void setViewLayoutMarginDimen(@IdRes int viewId, @MarginType int type, @DimenRes int dimen) { addAction(new LayoutParamAction(viewId, type, dimen, VALUE_TYPE_RESOURCE)); } /** * Equivalent to calling {@link MarginLayoutParams#setMarginEnd}. * Only works if the {@link View#getLayoutParams()} supports margins. * * @param viewId The id of the view to change * @param type The margin being set e.g. {@link #MARGIN_END} * @param attr a dimension attribute to apply to the margin, or 0 to clear the margin. */ public void setViewLayoutMarginAttr(@IdRes int viewId, @MarginType int type, @AttrRes int attr) { addAction(new LayoutParamAction(viewId, type, attr, VALUE_TYPE_ATTRIBUTE)); } /** * Equivalent to calling {@link MarginLayoutParams#setMarginEnd}. * Only works if the {@link View#getLayoutParams()} supports margins. * *

NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0. * Setting margins in pixels will behave poorly when the RemoteViews object is used on a * display with a different density. * * @param viewId The id of the view to change * @param type The margin being set e.g. {@link #MARGIN_END} * @param value a value for the margin the given units. * @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP} */ public void setViewLayoutMargin(@IdRes int viewId, @MarginType int type, float value, @ComplexDimensionUnit int units) { addAction(new LayoutParamAction(viewId, type, value, units)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} except that you may * provide the value in any dimension units. * *

NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0, * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}. * Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a * display with a different density. * * @param width Width of the view in the given units * @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP} */ public void setViewLayoutWidth(@IdRes int viewId, float width, @ComplexDimensionUnit int units) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, width, units)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with * the result of {@link Resources#getDimensionPixelSize(int)}. * * @param widthDimen the dimension resource for the view's width */ public void setViewLayoutWidthDimen(@IdRes int viewId, @DimenRes int widthDimen) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthDimen, VALUE_TYPE_RESOURCE)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with * the value of the given attribute in the current theme. * * @param widthAttr the dimension attribute for the view's width */ public void setViewLayoutWidthAttr(@IdRes int viewId, @AttrRes int widthAttr) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthAttr, VALUE_TYPE_ATTRIBUTE)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} except that you may * provide the value in any dimension units. * *

NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0, * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}. * Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a * display with a different density. * * @param height height of the view in the given units * @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP} */ public void setViewLayoutHeight(@IdRes int viewId, float height, @ComplexDimensionUnit int units) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, height, units)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with * the result of {@link Resources#getDimensionPixelSize(int)}. * * @param heightDimen a dimen resource to read the height from. */ public void setViewLayoutHeightDimen(@IdRes int viewId, @DimenRes int heightDimen) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightDimen, VALUE_TYPE_RESOURCE)); } /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with * the value of the given attribute in the current theme. * * @param heightAttr a dimen attribute to read the height from. */ public void setViewLayoutHeightAttr(@IdRes int viewId, @AttrRes int heightAttr) { addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightAttr, VALUE_TYPE_ATTRIBUTE)); } /** * Sets an OutlineProvider on the view whose corner radius is a dimension calculated using * {@link TypedValue#applyDimension(int, float, DisplayMetrics)}. * *

NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0. * Setting margins in pixels will behave poorly when the RemoteViews object is used on a * display with a different density. */ public void setViewOutlinePreferredRadius( @IdRes int viewId, float radius, @ComplexDimensionUnit int units) { addAction(new SetViewOutlinePreferredRadiusAction(viewId, radius, units)); } /** * Sets an OutlineProvider on the view whose corner radius is a dimension resource with * {@code resId}. */ public void setViewOutlinePreferredRadiusDimen(@IdRes int viewId, @DimenRes int resId) { addAction(new SetViewOutlinePreferredRadiusAction(viewId, resId, VALUE_TYPE_RESOURCE)); } /** * Sets an OutlineProvider on the view whose corner radius is a dimension attribute with * {@code attrId}. */ public void setViewOutlinePreferredRadiusAttr(@IdRes int viewId, @AttrRes int attrId) { addAction(new SetViewOutlinePreferredRadiusAction(viewId, attrId, VALUE_TYPE_ATTRIBUTE)); } /** * Call a method taking one boolean on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setBoolean(@IdRes int viewId, String methodName, boolean value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BOOLEAN, value)); } /** * Call a method taking one byte on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setByte(@IdRes int viewId, String methodName, byte value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BYTE, value)); } /** * Call a method taking one short on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setShort(@IdRes int viewId, String methodName, short value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.SHORT, value)); } /** * Call a method taking one int on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setInt(@IdRes int viewId, String methodName, int value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value)); } /** * Call a method taking one int, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the resources at the time the {@link RemoteViews} is * (re-)applied. * * Undefined resources will result in an exception, except 0 which will resolve to 0. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param dimenResource The resource to resolve and pass as argument to the method. */ public void setIntDimen(@IdRes int viewId, @NonNull String methodName, @DimenRes int dimenResource) { addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT, ResourceReflectionAction.DIMEN_RESOURCE, dimenResource)); } /** * Call a method taking one int, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the specified dimension at the time of inflation. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value of the dimension. * @param unit The unit in which the value is specified. */ public void setIntDimen(@IdRes int viewId, @NonNull String methodName, float value, @ComplexDimensionUnit int unit) { addAction(new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.INT, value, unit)); } /** * Call a method taking one int, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the theme attribute at the time the * {@link RemoteViews} is (re-)applied. * * Unresolvable attributes will result in an exception, except 0 which will resolve to 0. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param dimenAttr The attribute to resolve and pass as argument to the method. */ public void setIntDimenAttr(@IdRes int viewId, @NonNull String methodName, @AttrRes int dimenAttr) { addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT, ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr)); } /** * Call a method taking one int, a color, on a view in the layout for this RemoteViews. * * The Color will be resolved from the resources at the time the {@link RemoteViews} is (re-) * applied. * * Undefined resources will result in an exception, except 0 which will resolve to 0. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param colorResource The resource to resolve and pass as argument to the method. */ public void setColor(@IdRes int viewId, @NonNull String methodName, @ColorRes int colorResource) { addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT, ResourceReflectionAction.COLOR_RESOURCE, colorResource)); } /** * Call a method taking one int, a color, on a view in the layout for this RemoteViews. * * The Color will be resolved from the theme attribute at the time the {@link RemoteViews} is * (re-)applied. * * Unresolvable attributes will result in an exception, except 0 which will resolve to 0. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param colorAttribute The theme attribute to resolve and pass as argument to the method. */ public void setColorAttr(@IdRes int viewId, @NonNull String methodName, @AttrRes int colorAttribute) { addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT, AttributeReflectionAction.COLOR_RESOURCE, colorAttribute)); } /** * Call a method taking one int, a color, on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param notNight The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_NO} * @param night The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_YES} */ public void setColorInt( @IdRes int viewId, @NonNull String methodName, @ColorInt int notNight, @ColorInt int night) { addAction( new NightModeReflectionAction( viewId, methodName, BaseReflectionAction.INT, notNight, night)); } /** * Call a method taking one ColorStateList on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setColorStateList(@IdRes int viewId, @NonNull String methodName, @Nullable ColorStateList value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST, value)); } /** * Call a method taking one ColorStateList on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param notNight The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_NO} * @param night The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_YES} */ public void setColorStateList( @IdRes int viewId, @NonNull String methodName, @Nullable ColorStateList notNight, @Nullable ColorStateList night) { addAction( new NightModeReflectionAction( viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST, notNight, night)); } /** * Call a method taking one ColorStateList on a view in the layout for this RemoteViews. * * The ColorStateList will be resolved from the resources at the time the {@link RemoteViews} is * (re-)applied. * * Undefined resources will result in an exception, except 0 which will resolve to null. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param colorResource The resource to resolve and pass as argument to the method. */ public void setColorStateList(@IdRes int viewId, @NonNull String methodName, @ColorRes int colorResource) { addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE, colorResource)); } /** * Call a method taking one ColorStateList on a view in the layout for this RemoteViews. * * The ColorStateList will be resolved from the theme attribute at the time the * {@link RemoteViews} is (re-)applied. * * Unresolvable attributes will result in an exception, except 0 which will resolve to null. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param colorAttr The theme attribute to resolve and pass as argument to the method. */ public void setColorStateListAttr(@IdRes int viewId, @NonNull String methodName, @AttrRes int colorAttr) { addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE, colorAttr)); } /** * Call a method taking one long on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setLong(@IdRes int viewId, String methodName, long value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.LONG, value)); } /** * Call a method taking one float on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setFloat(@IdRes int viewId, String methodName, float value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT, value)); } /** * Call a method taking one float, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the resources at the time the {@link RemoteViews} is * (re-)applied. * * Undefined resources will result in an exception, except 0 which will resolve to 0f. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param dimenResource The resource to resolve and pass as argument to the method. */ public void setFloatDimen(@IdRes int viewId, @NonNull String methodName, @DimenRes int dimenResource) { addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT, ResourceReflectionAction.DIMEN_RESOURCE, dimenResource)); } /** * Call a method taking one float, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the resources at the time the {@link RemoteViews} is * (re-)applied. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value of the dimension. * @param unit The unit in which the value is specified. */ public void setFloatDimen(@IdRes int viewId, @NonNull String methodName, float value, @ComplexDimensionUnit int unit) { addAction( new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value, unit)); } /** * Call a method taking one float, a size in pixels, on a view in the layout for this * RemoteViews. * * The dimension will be resolved from the theme attribute at the time the {@link RemoteViews} * is (re-)applied. * * Unresolvable attributes will result in an exception, except 0 which will resolve to 0f. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param dimenAttr The attribute to resolve and pass as argument to the method. */ public void setFloatDimenAttr(@IdRes int viewId, @NonNull String methodName, @AttrRes int dimenAttr) { addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT, ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr)); } /** * Call a method taking one double on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setDouble(@IdRes int viewId, String methodName, double value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.DOUBLE, value)); } /** * Call a method taking one char on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setChar(@IdRes int viewId, String methodName, char value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR, value)); } /** * Call a method taking one String on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setString(@IdRes int viewId, String methodName, String value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.STRING, value)); } /** * Call a method taking one CharSequence on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setCharSequence(@IdRes int viewId, String methodName, CharSequence value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE, value)); } /** * Call a method taking one CharSequence on a view in the layout for this RemoteViews. * * The CharSequence will be resolved from the resources at the time the {@link RemoteViews} is * (re-)applied. * * Undefined resources will result in an exception, except 0 which will resolve to null. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param stringResource The resource to resolve and pass as argument to the method. */ public void setCharSequence(@IdRes int viewId, @NonNull String methodName, @StringRes int stringResource) { addAction( new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE, ResourceReflectionAction.STRING_RESOURCE, stringResource)); } /** * Call a method taking one CharSequence on a view in the layout for this RemoteViews. * * The CharSequence will be resolved from the theme attribute at the time the * {@link RemoteViews} is (re-)applied. * * Unresolvable attributes will result in an exception, except 0 which will resolve to null. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param stringAttribute The attribute to resolve and pass as argument to the method. */ public void setCharSequenceAttr(@IdRes int viewId, @NonNull String methodName, @AttrRes int stringAttribute) { addAction( new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE, AttributeReflectionAction.STRING_RESOURCE, stringAttribute)); } /** * Call a method taking one Uri on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setUri(@IdRes int viewId, String methodName, Uri value) { if (value != null) { // Resolve any filesystem path before sending remotely value = value.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { value.checkFileUriExposed("RemoteViews.setUri()"); } } addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.URI, value)); } /** * Call a method taking one Bitmap on a view in the layout for this RemoteViews. * @more *

The bitmap will be flattened into the parcel if this object is * sent across processes, so it may end up using a lot of memory, and may be fairly slow.

* * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setBitmap(@IdRes int viewId, String methodName, Bitmap value) { addAction(new BitmapReflectionAction(viewId, methodName, value)); } /** * Call a method taking one BlendMode on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setBlendMode(@IdRes int viewId, @NonNull String methodName, @Nullable BlendMode value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BLEND_MODE, value)); } /** * Call a method taking one Bundle on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The value to pass to the method. */ public void setBundle(@IdRes int viewId, String methodName, Bundle value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BUNDLE, value)); } /** * Call a method taking one Intent on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The {@link android.content.Intent} to pass the method. */ public void setIntent(@IdRes int viewId, String methodName, Intent value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INTENT, value)); } /** * Call a method taking one Icon on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param value The {@link android.graphics.drawable.Icon} to pass the method. */ public void setIcon(@IdRes int viewId, String methodName, Icon value) { addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.ICON, value)); } /** * Call a method taking one Icon on a view in the layout for this RemoteViews. * * @param viewId The id of the view on which to call the method. * @param methodName The name of the method to call. * @param notNight The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_NO} * @param night The value to pass to the method when the view's configuration is set to * {@link Configuration#UI_MODE_NIGHT_YES} */ public void setIcon( @IdRes int viewId, @NonNull String methodName, @Nullable Icon notNight, @Nullable Icon night) { addAction( new NightModeReflectionAction( viewId, methodName, BaseReflectionAction.ICON, notNight, night)); } /** * Equivalent to calling View.setContentDescription(CharSequence). * * @param viewId The id of the view whose content description should change. * @param contentDescription The new content description for the view. */ public void setContentDescription(@IdRes int viewId, CharSequence contentDescription) { setCharSequence(viewId, "setContentDescription", contentDescription); } /** * Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}. * * @param viewId The id of the view whose before view in accessibility traversal to set. * @param nextId The id of the next in the accessibility traversal. **/ public void setAccessibilityTraversalBefore(@IdRes int viewId, @IdRes int nextId) { setInt(viewId, "setAccessibilityTraversalBefore", nextId); } /** * Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}. * * @param viewId The id of the view whose after view in accessibility traversal to set. * @param nextId The id of the next in the accessibility traversal. **/ public void setAccessibilityTraversalAfter(@IdRes int viewId, @IdRes int nextId) { setInt(viewId, "setAccessibilityTraversalAfter", nextId); } /** * Equivalent to calling {@link View#setLabelFor(int)}. * * @param viewId The id of the view whose property to set. * @param labeledId The id of a view for which this view serves as a label. */ public void setLabelFor(@IdRes int viewId, @IdRes int labeledId) { setInt(viewId, "setLabelFor", labeledId); } /** * Equivalent to calling {@link android.widget.CompoundButton#setChecked(boolean)}. * * @param viewId The id of the view whose property to set. * @param checked true to check the button, false to uncheck it. */ public void setCompoundButtonChecked(@IdRes int viewId, boolean checked) { addAction(new SetCompoundButtonCheckedAction(viewId, checked)); } /** * Equivalent to calling {@link android.widget.RadioGroup#check(int)}. * * @param viewId The id of the view whose property to set. * @param checkedId The unique id of the radio button to select in the group. */ public void setRadioGroupChecked(@IdRes int viewId, @IdRes int checkedId) { addAction(new SetRadioGroupCheckedAction(viewId, checkedId)); } /** * Provides an alternate layout ID, which can be used to inflate this view. This layout will be * used by the host when the widgets displayed on a light-background where foreground elements * and text can safely draw using a dark color without any additional background protection. */ public void setLightBackgroundLayoutId(@LayoutRes int layoutId) { mLightBackgroundLayoutId = layoutId; } /** * If this view supports dark text versions, creates a copy representing that version, * otherwise returns itself. * @hide */ public RemoteViews getDarkTextViews() { if (hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT)) { return this; } try { addFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT); return new RemoteViews(this); } finally { mApplyFlags &= ~FLAG_USE_LIGHT_BACKGROUND_LAYOUT; } } private boolean hasDrawInstructions() { return mHasDrawInstructions; } private RemoteViews getRemoteViewsToApply(Context context) { if (hasLandscapeAndPortraitLayouts()) { int orientation = context.getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { return mLandscape; } 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(SizeF p1, SizeF p2) { float dx = p1.getWidth() - p2.getWidth(); float dy = p1.getHeight() - p2.getHeight(); 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(SizeF sizeLayout, @Nullable SizeF sizeWidget) { return sizeWidget != null && (Math.ceil(sizeWidget.getWidth()) + 1 > sizeLayout.getWidth()) && (Math.ceil(sizeWidget.getHeight()) + 1 > sizeLayout.getHeight()); } private RemoteViews findBestFitLayout(@NonNull SizeF widgetSize) { // Find the better remote view RemoteViews bestFit = null; float bestSqDist = Float.MAX_VALUE; for (RemoteViews layout : mSizedRemoteViews) { SizeF layoutSize = layout.getIdealSize(); if (layoutSize == null) { throw new IllegalStateException("Expected RemoteViews to have ideal size"); } 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; } /** * 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. * * @hide */ public RemoteViews getRemoteViewsToApply(@NonNull Context context, @Nullable SizeF widgetSize) { if (!hasSizedRemoteViews() || widgetSize == null) { // If there isn't multiple remote views, fall back on the previous methods. return getRemoteViewsToApply(context); } return findBestFitLayout(widgetSize); } /** * Checks whether the change of size will lead to using a different {@link RemoteViews}. * * @hide */ @Nullable public RemoteViews getRemoteViewsToApplyIfDifferent(@Nullable SizeF oldSize, @NonNull SizeF newSize) { if (!hasSizedRemoteViews()) { return null; } RemoteViews oldBestFit = oldSize == null ? findSmallestRemoteView() : findBestFitLayout( oldSize); RemoteViews newBestFit = findBestFitLayout(newSize); if (oldBestFit != newBestFit) { return newBestFit; } return null; } /** * Inflates the view hierarchy represented by this object and applies * all of the actions. * *

Caller beware: this may throw * * @param context Default context to use * @param parent Parent that the resulting view hierarchy will be attached to. This method * does not attach the hierarchy. The caller should do so when appropriate. * @return The inflated view hierarchy */ public View apply(Context context, ViewGroup parent) { return apply(context, parent, null); } /** @hide */ public View apply(Context context, ViewGroup parent, InteractionHandler handler) { return apply(context, parent, handler, null); } /** @hide */ public View apply(@NonNull Context context, @NonNull ViewGroup parent, @Nullable InteractionHandler handler, @Nullable SizeF size) { return apply(context, parent, size, new ActionApplyParams() .withInteractionHandler(handler)); } /** @hide */ public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent, @Nullable InteractionHandler handler, @StyleRes int applyThemeResId) { return apply(context, parent, null, new ActionApplyParams() .withInteractionHandler(handler) .withThemeResId(applyThemeResId)); } /** @hide */ public View apply(Context context, ViewGroup parent, InteractionHandler handler, @Nullable SizeF size, @Nullable ColorResources colorResources) { return apply(context, parent, size, new ActionApplyParams() .withInteractionHandler(handler) .withColorResources(colorResources)); } /** @hide **/ public View apply(Context context, ViewGroup parent, @Nullable SizeF size, ActionApplyParams params) { return apply(context, parent, parent, size, params); } private View apply(Context context, ViewGroup directParent, ViewGroup rootParent, @Nullable SizeF size, ActionApplyParams params) { RemoteViews rvToApply = getRemoteViewsToApply(context, size); View result = inflateView(context, rvToApply, directParent, params.applyThemeResId, params.colorResources); rvToApply.performApply(result, rootParent, params); return result; } private View inflateView(Context context, RemoteViews rv, @Nullable ViewGroup parent, @StyleRes int applyThemeResId, @Nullable ColorResources colorResources) { // RemoteViews may be built by an application installed in another // user. So build a context that loads resources from that user but // still returns the current users userId so settings like data / time formats // are loaded without requiring cross user persmissions. final Context contextForResources = getContextForResourcesEnsuringCorrectCachedApkPaths(context); if (colorResources != null) { colorResources.apply(contextForResources); } Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources); // If mApplyThemeResId is not given, Theme.DeviceDefault will be used. if (applyThemeResId != 0) { inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId); } View v; // If the RemoteViews contains draw instructions, just use it instead. if (rv.hasDrawInstructions()) { final RemoteComposePlayer player = new RemoteComposePlayer(inflationContext); player.setDebug(Build.IS_USERDEBUG || Build.IS_ENG ? 1 : 0); v = player; } else { LayoutInflater inflater = LayoutInflater.from(context); // Clone inflater so we load resources from correct context and // we don't add a filter to the static version returned by getSystemService. inflater = inflater.cloneInContext(inflationContext); inflater.setFilter(shouldUseStaticFilter() ? INFLATER_FILTER : this); if (mLayoutInflaterFactory2 != null) { inflater.setFactory2(mLayoutInflaterFactory2); } v = inflater.inflate(rv.getLayoutId(), parent, false); } if (mViewId != View.NO_ID) { v.setId(mViewId); v.setTagInternal(R.id.remote_views_override_id, mViewId); } v.setTagInternal(R.id.widget_frame, rv.getLayoutId()); return v; } /** * A static filter is much lighter than RemoteViews itself. It's optimized here only for * RemoteVies class. Subclasses should always override this and return true if not overriding * {@link this#onLoadClass(Class)}. * * @hide */ protected boolean shouldUseStaticFilter() { return this.getClass().equals(RemoteViews.class); } /** * Implement this interface to receive a callback when * {@link #applyAsync} or {@link #reapplyAsync} is finished. * @hide */ public interface OnViewAppliedListener { /** * Callback when the RemoteView has finished inflating, * but no actions have been applied yet. */ default void onViewInflated(View v) {} void onViewApplied(View v); void onError(Exception e); } /** * Applies the views asynchronously, moving as much of the task on the background * thread as possible. * * @see #apply(Context, ViewGroup) * @param context Default context to use * @param parent Parent that the resulting view hierarchy will be attached to. This method * does not attach the hierarchy. The caller should do so when appropriate. * @param listener the callback to run when all actions have been applied. May be null. * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used. * @return CancellationSignal * @hide */ public CancellationSignal applyAsync( Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) { return applyAsync(context, parent, executor, listener, null /* handler */); } /** @hide */ public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, InteractionHandler handler) { return applyAsync(context, parent, executor, listener, handler, null /* size */); } /** @hide */ public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, InteractionHandler handler, SizeF size) { return applyAsync(context, parent, executor, listener, handler, size, null /* themeColors */); } /** @hide */ public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, InteractionHandler handler, SizeF size, ColorResources colorResources) { ActionApplyParams params = new ActionApplyParams() .withInteractionHandler(handler) .withColorResources(colorResources) .withExecutor(executor); return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener, params, null /* result */, true /* topLevel */).startTaskOnExecutor(executor); } private AsyncApplyTask getInternalAsyncApplyTask(Context context, ViewGroup parent, OnViewAppliedListener listener, ActionApplyParams params, SizeF size, View result) { return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener, params, result, false /* topLevel */); } private class AsyncApplyTask extends AsyncTask implements CancellationSignal.OnCancelListener { final CancellationSignal mCancelSignal = new CancellationSignal(); final RemoteViews mRV; final ViewGroup mParent; final Context mContext; final OnViewAppliedListener mListener; final ActionApplyParams mApplyParams; /** * Whether the remote view is the top-level one (i.e. not within an action). * * This is only used if the result is specified (i.e. the view is being recycled). */ final boolean mTopLevel; private View mResult; private ViewTree mTree; private Action[] mActions; private Exception mError; private AsyncApplyTask( RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener, ActionApplyParams applyParams, View result, boolean topLevel) { mRV = rv; mParent = parent; mContext = context; mListener = listener; mTopLevel = topLevel; mApplyParams = applyParams; mResult = result; } @Nullable @Override protected ViewTree doInBackground(Void... params) { try { if (mResult == null) { mResult = inflateView(mContext, mRV, mParent, 0, mApplyParams.colorResources); } mTree = new ViewTree(mResult); if (mRV.mActions != null) { int count = mRV.mActions.size(); mActions = new Action[count]; for (int i = 0; i < count && !isCancelled(); i++) { // TODO: check if isCancelled in nested views. mActions[i] = mRV.mActions.get(i) .initActionAsync(mTree, mParent, mApplyParams); } } else { mActions = null; } return mTree; } catch (Exception e) { mError = e; return null; } } @Override protected void onPostExecute(ViewTree viewTree) { mCancelSignal.setOnCancelListener(null); if (mError == null) { if (mListener != null) { mListener.onViewInflated(viewTree.mRoot); } try { if (mActions != null) { ActionApplyParams applyParams = mApplyParams.clone(); if (applyParams.handler == null) { applyParams.handler = DEFAULT_INTERACTION_HANDLER; } for (Action a : mActions) { a.apply(viewTree.mRoot, mParent, applyParams); } } // If the parent of the view is has is a root, resolve the recycling. if (mTopLevel && mResult instanceof ViewGroup) { finalizeViewRecycling((ViewGroup) mResult); } } catch (Exception e) { mError = e; } } if (mListener != null) { if (mError != null) { mListener.onError(mError); } else { mListener.onViewApplied(viewTree.mRoot); } } else if (mError != null) { if (mError instanceof ActionException) { throw (ActionException) mError; } else { throw new ActionException(mError); } } } @Override public void onCancel() { cancel(true); } private CancellationSignal startTaskOnExecutor(Executor executor) { mCancelSignal.setOnCancelListener(this); executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor); return mCancelSignal; } } /** * Applies all of the actions to the provided view. * *

Caller beware: this may throw * * @param v The view to apply the actions to. This should be the result of * the {@link #apply(Context,ViewGroup)} call. */ public void reapply(Context context, View v) { reapply(context, v, null /* size */, new ActionApplyParams()); } /** @hide */ public void reapply(Context context, View v, InteractionHandler handler) { reapply(context, v, null /* size */, new ActionApplyParams().withInteractionHandler(handler)); } /** @hide */ public void reapply(Context context, View v, InteractionHandler handler, SizeF size, ColorResources colorResources) { reapply(context, v, size, new ActionApplyParams() .withInteractionHandler(handler).withColorResources(colorResources)); } /** @hide */ public void reapply(Context context, View v, @Nullable SizeF size, ActionApplyParams params) { reapply(context, v, (ViewGroup) v.getParent(), size, params, true); } private void reapplyNestedViews(Context context, View v, ViewGroup rootParent, ActionApplyParams params) { reapply(context, v, rootParent, null, params, false); } // Note: topLevel should be true only for calls on the topLevel RemoteViews, internal calls // should set it to false. private void reapply(Context context, View v, ViewGroup rootParent, @Nullable SizeF size, ActionApplyParams params, boolean topLevel) { RemoteViews rvToApply = getRemoteViewsToReapply(context, v, size); rvToApply.performApply(v, rootParent, params); // If the parent of the view is has is a root, resolve the recycling. if (topLevel && v instanceof ViewGroup) { finalizeViewRecycling((ViewGroup) v); } } /** @hide */ public boolean canRecycleView(@Nullable View v) { if (v == null || hasDrawInstructions()) { return false; } Integer previousLayoutId = (Integer) v.getTag(R.id.widget_frame); if (previousLayoutId == null) { return false; } Integer overrideIdTag = (Integer) v.getTag(R.id.remote_views_override_id); int overrideId = overrideIdTag == null ? View.NO_ID : overrideIdTag; // If mViewId is View.NO_ID, we only recycle if overrideId is also View.NO_ID. // Otherwise, it might be that, on a previous iteration, the view's ID was set to // something else, and it should now be reset to the ID defined in the XML layout file, // whatever it is. return previousLayoutId == getLayoutId() && mViewId == overrideId; } /** * Returns the RemoteViews that should be used in the reapply operation. * * If the current RemoteViews has multiple layout, this will select the correct one. * * @throws RuntimeException If the current RemoteViews should not be reapplied onto the provided * View. */ private RemoteViews getRemoteViewsToReapply(Context context, View v, @Nullable SizeF 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 the ViewID has been changed on the view, or is changed by the RemoteViews, we also // may throw an exception, as the RemoteViews will probably not apply properly. // However, we need to let potentially unrelated RemoteViews apply, as this lack of testing // is already used in production code in some apps. if (hasMultipleLayouts() || rvToApply.mViewId != View.NO_ID || v.getTag(R.id.remote_views_override_id) != null) { if (!rvToApply.canRecycleView(v)) { throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" + " that does not share the same root layout id."); } } return rvToApply; } /** * Applies all the actions to the provided view, moving as much of the task on the background * thread as possible. * * @see #reapply(Context, View) * @param context Default context to use * @param v The view to apply the actions to. This should be the result of * the {@link #apply(Context,ViewGroup)} call. * @param listener the callback to run when all actions have been applied. May be null. * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used * @return CancellationSignal * @hide */ public CancellationSignal reapplyAsync(Context context, View v, Executor executor, OnViewAppliedListener listener) { return reapplyAsync(context, v, executor, listener, null); } /** @hide */ public CancellationSignal reapplyAsync(Context context, View v, Executor executor, OnViewAppliedListener listener, InteractionHandler handler) { return reapplyAsync(context, v, executor, listener, handler, null, null); } /** @hide */ public CancellationSignal reapplyAsync(Context context, View v, Executor executor, OnViewAppliedListener listener, InteractionHandler handler, SizeF size, ColorResources colorResources) { RemoteViews rvToApply = getRemoteViewsToReapply(context, v, size); ActionApplyParams params = new ActionApplyParams() .withColorResources(colorResources) .withInteractionHandler(handler) .withExecutor(executor); return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(), context, listener, params, v, true /* topLevel */) .startTaskOnExecutor(executor); } private void performApply(View v, ViewGroup parent, ActionApplyParams params) { params = params.clone(); if (params.handler == null) { params.handler = DEFAULT_INTERACTION_HANDLER; } if (v instanceof RemoteComposePlayer player) { player.setTheme(v.getResources().getConfiguration().isNightModeActive() ? Theme.DARK : Theme.LIGHT); } if (mActions != null) { final int count = mActions.size(); for (int i = 0; i < count; i++) { mActions.get(i).apply(v, parent, params); } } } /** * Returns true if the RemoteViews contains potentially costly operations and should be * applied asynchronously. * * @hide */ public boolean prefersAsyncApply() { if (mActions != null) { final int count = mActions.size(); for (int i = 0; i < count; i++) { if (mActions.get(i).prefersAsyncApply()) { return true; } } } return false; } /** @hide */ public void updateAppInfo(@NonNull ApplicationInfo info) { ApplicationInfo existing = mApplicationInfoCache.get(info); if (existing != null && !existing.sourceDir.equals(info.sourceDir)) { // Overlay paths are generated against a particular version of an application. // The overlays paths of a newly upgraded application are incompatible with the // old version of the application. return; } // If we can update to the new AppInfo, put it in the cache and propagate the change // throughout the hierarchy. mApplicationInfoCache.put(info); configureDescendantsAsChildren(); } private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) { if (mApplication != null) { if (context.getUserId() == UserHandle.getUserId(mApplication.uid) && context.getPackageName().equals(mApplication.packageName)) { return context; } try { LoadedApk.checkAndUpdateApkPaths(mApplication); return context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED); } catch (NameNotFoundException e) { Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found"); } } return context; } @NonNull private SparseArray getPendingIntentTemplate() { if (mPendingIntentTemplate == null) { mPendingIntentTemplate = new SparseArray<>(); } return mPendingIntentTemplate; } @NonNull private SparseArray getFillInIntent() { if (mFillInIntent == null) { mFillInIntent = new SparseArray<>(); } return mFillInIntent; } private void tryAddRemoteResponse(final int viewId) { final PendingIntent pendingIntent = getPendingIntentTemplate().get(viewId); final Intent intent = getFillInIntent().get(viewId); if (pendingIntent != null && intent != null) { addAction(new SetOnClickResponse(viewId, RemoteResponse.fromPendingIntentTemplateAndFillInIntent( pendingIntent, intent))); } } /** * Utility class to hold all the options when applying the remote views * @hide */ public class ActionApplyParams { public InteractionHandler handler; public ColorResources colorResources; public Executor executor; @StyleRes public int applyThemeResId; @Override public ActionApplyParams clone() { return new ActionApplyParams() .withInteractionHandler(handler) .withColorResources(colorResources) .withExecutor(executor) .withThemeResId(applyThemeResId); } public ActionApplyParams withInteractionHandler(InteractionHandler handler) { this.handler = handler; return this; } public ActionApplyParams withColorResources(ColorResources colorResources) { this.colorResources = colorResources; return this; } public ActionApplyParams withThemeResId(@StyleRes int themeResId) { this.applyThemeResId = themeResId; return this; } public ActionApplyParams withExecutor(Executor executor) { this.executor = executor; return this; } } /** * Object allowing the modification of a context to overload the system's dynamic colors. * * Only colors from {@link android.R.color#system_accent1_0} to * {@link android.R.color#system_neutral2_1000} can be overloaded. * @hide */ public static final class ColorResources { // Set of valid colors resources. private static final int FIRST_RESOURCE_COLOR_ID = android.R.color.system_neutral1_0; private static final int LAST_RESOURCE_COLOR_ID = android.R.color.system_accent3_1000; // Size, in bytes, of an entry in the array of colors in an ARSC file. private static final int ARSC_ENTRY_SIZE = 16; private final ResourcesLoader mLoader; private final SparseIntArray mColorMapping; private ColorResources(ResourcesLoader loader, SparseIntArray colorMapping) { mLoader = loader; mColorMapping = colorMapping; } /** * Apply the color resources to the given context. * * No resource resolution must have be done on the context given to that method. */ public void apply(Context context) { context.getResources().addLoaders(mLoader); } public SparseIntArray getColorMapping() { return mColorMapping; } private static ByteArrayOutputStream readFileContent(InputStream input) throws IOException { ByteArrayOutputStream content = new ByteArrayOutputStream(2048); byte[] buffer = new byte[4096]; while (input.available() > 0) { int read = input.read(buffer); content.write(buffer, 0, read); } return content; } /** * Creates the compiled resources content from the asset stored in the APK. * * The asset is a compiled resource with the correct resources name and correct ids, only * the values are incorrect. The last value is at the very end of the file. The resources * are in an array, the array's entries are 16 bytes each. We use this to work out the * location of all the positions of the various resources. */ @Nullable private static byte[] createCompiledResourcesContent(Context context, SparseIntArray colorResources) throws IOException { byte[] content; try (InputStream input = context.getResources().openRawResource( com.android.internal.R.raw.remote_views_color_resources)) { ByteArrayOutputStream rawContent = readFileContent(input); content = rawContent.toByteArray(); } int valuesOffset = content.length - (LAST_RESOURCE_COLOR_ID & 0xffff) * ARSC_ENTRY_SIZE - 4; if (valuesOffset < 0) { Log.e(LOG_TAG, "ARSC file for theme colors is invalid."); return null; } for (int colorRes = FIRST_RESOURCE_COLOR_ID; colorRes <= LAST_RESOURCE_COLOR_ID; colorRes++) { // The last 2 bytes are the index in the color array. int index = colorRes & 0xffff; int offset = valuesOffset + index * ARSC_ENTRY_SIZE; int value = colorResources.get(colorRes, context.getColor(colorRes)); // Write the 32 bit integer in little endian for (int b = 0; b < 4; b++) { content[offset + b] = (byte) (value & 0xff); value >>= 8; } } return content; } /** * Adds a resource loader for theme colors to the given context. * * @param context Context of the view hosting the widget. * @param colorMapping Mapping of resources to color values. * * @hide */ @Nullable public static ColorResources create(Context context, SparseIntArray colorMapping) { try { byte[] contentBytes = createCompiledResourcesContent(context, colorMapping); if (contentBytes == null) { return null; } FileDescriptor arscFile = null; try { arscFile = Os.memfd_create("remote_views_theme_colors.arsc", 0 /* flags */); // Note: This must not be closed through the OutputStream. try (OutputStream pipeWriter = new FileOutputStream(arscFile)) { pipeWriter.write(contentBytes); try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(arscFile)) { ResourcesLoader colorsLoader = new ResourcesLoader(); colorsLoader.addProvider(ResourcesProvider .loadFromTable(pfd, null /* assetsProvider */)); return new ColorResources(colorsLoader, colorMapping.clone()); } } } finally { if (arscFile != null) { Os.close(arscFile); } } } catch (Exception ex) { Log.e(LOG_TAG, "Failed to setup the context for theme colors", ex); } return null; } } /** * Returns the number of actions in this RemoteViews. Can be used as a sequence number. * * @hide */ public int getSequenceNumber() { return (mActions == null) ? 0 : mActions.size(); } /** * Used to restrict the views which can be inflated * * @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class) * @deprecated Used by system to enforce safe inflation of {@link RemoteViews}. Apps should not * override this method. Changing of this method will NOT affect the process where RemoteViews * is rendered. */ @Deprecated public boolean onLoadClass(Class clazz) { return clazz.isAnnotationPresent(RemoteView.class); } public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { writeToParcel(dest, flags, /* intentsToIgnore= */ null); } private void writeToParcel(Parcel dest, int flags, @Nullable SparseArray intentsToIgnore) { boolean prevSquashingAllowed = dest.allowSquashing(); 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. if (mIsRoot) { mBitmapCache.writeBitmapsToParcel(dest, flags); mCollectionCache.writeToParcel(dest, flags, intentsToIgnore); } dest.writeTypedObject(mApplication, flags); if (mIsRoot || mIdealSize == null) { dest.writeInt(0); } else { dest.writeInt(1); mIdealSize.writeToParcel(dest, flags); } dest.writeInt(mLayoutId); dest.writeInt(mViewId); dest.writeInt(mLightBackgroundLayoutId); writeActionsToParcel(dest, flags); } else if (hasSizedRemoteViews()) { dest.writeInt(MODE_HAS_SIZED_REMOTEVIEWS); if (mIsRoot) { mBitmapCache.writeBitmapsToParcel(dest, flags); mCollectionCache.writeToParcel(dest, flags, intentsToIgnore); } dest.writeInt(mSizedRemoteViews.size()); for (RemoteViews view : mSizedRemoteViews) { view.writeToParcel(dest, flags); } } else { dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT); // We only write the bitmap cache if we are the root RemoteViews, as this cache // is shared by all children. if (mIsRoot) { mBitmapCache.writeBitmapsToParcel(dest, flags); mCollectionCache.writeToParcel(dest, flags, intentsToIgnore); } mLandscape.writeToParcel(dest, flags); // Both RemoteViews already share the same package and user mPortrait.writeToParcel(dest, flags); } dest.writeInt(mApplyFlags); dest.writeLong(mProviderInstanceId); dest.writeBoolean(mHasDrawInstructions); dest.restoreAllowSquashing(prevSquashingAllowed); } private void writeActionsToParcel(Parcel parcel, int flags) { int count; if (mActions != null) { count = mActions.size(); } else { count = 0; } parcel.writeInt(count); for (int i = 0; i < count; i++) { Action a = mActions.get(i); parcel.writeInt(a.getActionTag()); a.writeToParcel(parcel, flags); } } @Nullable private static ApplicationInfo getApplicationInfo(@Nullable String packageName, int userId) { if (packageName == null) { return null; } // Get the application for the passed in package and user. Application application = ActivityThread.currentApplication(); if (application == null) { throw new IllegalStateException("Cannot create remote views out of an aplication."); } ApplicationInfo applicationInfo = application.getApplicationInfo(); if (UserHandle.getUserId(applicationInfo.uid) != userId || !applicationInfo.packageName.equals(packageName)) { try { Context context = application.getBaseContext().createPackageContextAsUser( packageName, 0, new UserHandle(userId)); applicationInfo = context.getApplicationInfo(); } catch (NameNotFoundException nnfe) { throw new IllegalArgumentException("No such package " + packageName); } } return applicationInfo; } /** * Returns true if the {@link #mApplication} is same as the provided info. * * @hide */ public boolean hasSameAppInfo(ApplicationInfo info) { return mApplication == null || mApplication.packageName.equals(info.packageName) && mApplication.uid == info.uid; } /** * Parcelable.Creator that instantiates RemoteViews objects */ @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public RemoteViews createFromParcel(Parcel parcel) { return new RemoteViews(parcel); } public RemoteViews[] newArray(int size) { return new RemoteViews[size]; } }; /** * A representation of the view hierarchy. Only views which have a valid ID are added * and can be searched. */ private static class ViewTree { private static final int INSERT_AT_END_INDEX = -1; private View mRoot; private ArrayList mChildren; private ViewTree(View root) { mRoot = root; } public void createTree() { if (mChildren != null) { return; } mChildren = new ArrayList<>(); if (mRoot instanceof ViewGroup) { ViewGroup vg = (ViewGroup) mRoot; int count = vg.getChildCount(); for (int i = 0; i < count; i++) { addViewChild(vg.getChildAt(i)); } } } @Nullable public ViewTree findViewTreeById(@IdRes int id) { if (mRoot.getId() == id) { return this; } if (mChildren == null) { return null; } for (ViewTree tree : mChildren) { ViewTree result = tree.findViewTreeById(id); if (result != null) { return result; } } return null; } @Nullable public ViewTree findViewTreeParentOf(ViewTree child) { if (mChildren == null) { return null; } for (ViewTree tree : mChildren) { if (tree == child) { return this; } ViewTree result = tree.findViewTreeParentOf(child); if (result != null) { return result; } } return null; } public void replaceView(View v) { mRoot = v; mChildren = null; createTree(); } @Nullable public T findViewById(@IdRes int id) { if (mChildren == null) { return mRoot.findViewById(id); } ViewTree tree = findViewTreeById(id); return tree == null ? null : (T) tree.mRoot; } public void addChild(ViewTree child) { addChild(child, INSERT_AT_END_INDEX); } /** * Adds the given {@link ViewTree} as a child at the given index. * * @param index The position at which to add the child or -1 to add last. */ public void addChild(ViewTree child, int index) { if (mChildren == null) { mChildren = new ArrayList<>(); } child.createTree(); if (index == INSERT_AT_END_INDEX) { mChildren.add(child); return; } mChildren.add(index, child); } public void removeChildren(int start, int count) { if (mChildren != null) { for (int i = 0; i < count; i++) { mChildren.remove(start); } } } private void addViewChild(View v) { // ViewTree only contains Views which can be found using findViewById. // If isRootNamespace is true, this view is skipped. // @see ViewGroup#findViewTraversal(int) if (v.isRootNamespace()) { return; } final ViewTree target; // If the view has a valid id, i.e., if can be found using findViewById, add it to the // tree, otherwise skip this view and add its children instead. if (v.getId() != 0) { ViewTree tree = new ViewTree(v); mChildren.add(tree); target = tree; } else { target = this; } if (v instanceof ViewGroup) { if (target.mChildren == null) { target.mChildren = new ArrayList<>(); ViewGroup vg = (ViewGroup) v; int count = vg.getChildCount(); for (int i = 0; i < count; i++) { target.addViewChild(vg.getChildAt(i)); } } } } /** Find the first child for which the condition is true and return its index. */ public int findChildIndex(Predicate condition) { return findChildIndex(0, condition); } /** * Find the first child, starting at {@code startIndex}, for which the condition is true and * return its index. */ public int findChildIndex(int startIndex, Predicate condition) { if (mChildren == null) { return -1; } for (int i = startIndex; i < mChildren.size(); i++) { if (condition.test(mChildren.get(i).mRoot)) { return i; } } return -1; } } /** * Class representing a response to an action performed on any element of a RemoteViews. */ public static class RemoteResponse { /** @hide **/ @IntDef(prefix = "INTERACTION_TYPE_", value = { INTERACTION_TYPE_CLICK, INTERACTION_TYPE_CHECKED_CHANGE, }) @Retention(RetentionPolicy.SOURCE) @interface InteractionType {} /** @hide */ public static final int INTERACTION_TYPE_CLICK = 0; /** @hide */ public static final int INTERACTION_TYPE_CHECKED_CHANGE = 1; private PendingIntent mPendingIntent; private Intent mFillIntent; private int mInteractionType = INTERACTION_TYPE_CLICK; private IntArray mViewIds; private ArrayList mElementNames; /** * Creates a response which sends a pending intent as part of the response. The source * bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the * target view in screen space. * Note that any activity options associated with the mPendingIntent may get overridden * before starting the intent. * * @param pendingIntent The {@link PendingIntent} to send as part of the response */ @NonNull public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) { RemoteResponse response = new RemoteResponse(); response.mPendingIntent = pendingIntent; return response; } /** * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is * very costly to set PendingIntents on the individual items, and is hence not recommended. * Instead a single PendingIntent template can be set on the collection, see {@link * RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click * action of a given item can be distinguished by setting a fillInIntent on that item. The * fillInIntent is then combined with the PendingIntent template in order to determine the * final intent which will be executed when the item is clicked. This works as follows: any * fields which are left blank in the PendingIntent template, but are provided by the * fillInIntent will be overwritten, and the resulting PendingIntent will be used. The rest * of the PendingIntent template will then be filled in with the associated fields that are * set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details. * Creates a response which sends a pending intent as part of the response. The source * bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the * target view in screen space. * Note that any activity options associated with the mPendingIntent may get overridden * 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 * @see RemoteViews#setPendingIntentTemplate(int, PendingIntent) * @see RemoteViews#setOnClickFillInIntent(int, Intent) */ @NonNull public static RemoteResponse fromFillInIntent(@NonNull Intent fillIntent) { RemoteResponse response = new RemoteResponse(); response.mFillIntent = fillIntent; return response; } private static RemoteResponse fromPendingIntentTemplateAndFillInIntent( @NonNull final PendingIntent pendingIntent, @NonNull final Intent intent) { RemoteResponse response = new RemoteResponse(); response.mPendingIntent = pendingIntent; response.mFillIntent = intent; return response; } /** * Adds a shared element to be transferred as part of the transition between Activities * using cross-Activity scene animations. The position of the first element will be used as * 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 sharedElementName The shared element name for this view * @see ActivityOptions#makeSceneTransitionAnimation(Activity, Pair[]) */ @NonNull public RemoteResponse addSharedElement(@IdRes int viewId, @NonNull String sharedElementName) { if (mViewIds == null) { mViewIds = new IntArray(); mElementNames = new ArrayList<>(); } mViewIds.add(viewId); mElementNames.add(sharedElementName); return this; } /** * Sets the interaction type for which this RemoteResponse responds. * * @param type the type of interaction for which this is a response, such as clicking or * checked state changing * * @hide */ @NonNull public RemoteResponse setInteractionType(@InteractionType int type) { mInteractionType = type; return this; } private void writeToParcel(Parcel dest, int flags) { PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest); dest.writeBoolean((mFillIntent != null)); if (mFillIntent != null) { dest.writeTypedObject(mFillIntent, flags); } dest.writeInt(mInteractionType); dest.writeIntArray(mViewIds == null ? null : mViewIds.toArray()); dest.writeStringList(mElementNames); } private void readFromParcel(Parcel parcel) { mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); mFillIntent = parcel.readBoolean() ? parcel.readTypedObject(Intent.CREATOR) : null; mInteractionType = parcel.readInt(); int[] viewIds = parcel.createIntArray(); mViewIds = viewIds == null ? null : IntArray.wrap(viewIds); mElementNames = parcel.createStringArrayList(); } /** * See {@link RemoteViews#visitUris(Consumer)}. * * @hide */ public void visitUris(@NonNull Consumer visitor) { if (mPendingIntent != null) { mPendingIntent.visitUris(visitor); } if (mFillIntent != null) { mFillIntent.visitUris(visitor); } } private void handleViewInteraction( View v, InteractionHandler handler) { final PendingIntent pi; if (mPendingIntent != null) { pi = mPendingIntent; } else if (mFillIntent != null) { AdapterView ancestor = getAdapterViewAncestor(v); if (ancestor == null) { Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent"); return; } // Ensure that a template pending intent has been set on the ancestor if (!(ancestor.getTag() instanceof PendingIntent)) { Log.e(LOG_TAG, "Attempting setOnClickFillInIntent or " + "setOnCheckedChangeFillInIntent without calling " + "setPendingIntentTemplate on parent."); return; } pi = (PendingIntent) ancestor.getTag(); } else { Log.e(LOG_TAG, "Response has neither pendingIntent nor fillInIntent"); return; } handler.onInteraction(v, pi, this); } /** * Returns the closest ancestor of the view that is an AdapterView or null if none could be * found. */ @Nullable private static AdapterView getAdapterViewAncestor(@Nullable View view) { if (view == null) return null; View parent = (View) view.getParent(); // Break the for loop on the first encounter of: // 1) an AdapterView, // 2) an AppWidgetHostView that is not a child of an adapter view, or // 3) a null parent. // 2) and 3) are unexpected and catch the case where a child is not // correctly parented in an AdapterView. while (parent != null && !(parent instanceof AdapterView) && !((parent instanceof AppWidgetHostView) && !(parent instanceof AppWidgetHostView.AdapterChildHostView))) { parent = (View) parent.getParent(); } return parent instanceof AdapterView ? (AdapterView) parent : null; } /** @hide */ public Pair getLaunchOptions(View view) { Intent intent = mFillIntent == null ? new Intent() : new Intent(mFillIntent); intent.setSourceBounds(getSourceBounds(view)); if (view instanceof CompoundButton && mInteractionType == INTERACTION_TYPE_CHECKED_CHANGE) { intent.putExtra(EXTRA_CHECKED, ((CompoundButton) view).isChecked()); } ActivityOptions opts = null; Context context = view.getContext(); if (context.getResources().getBoolean( com.android.internal.R.bool.config_overrideRemoteViewsActivityTransition)) { TypedArray windowStyle = context.getTheme().obtainStyledAttributes( com.android.internal.R.styleable.Window); int windowAnimations = windowStyle.getResourceId( com.android.internal.R.styleable.Window_windowAnimationStyle, 0); TypedArray windowAnimationStyle = context.obtainStyledAttributes( windowAnimations, com.android.internal.R.styleable.WindowAnimation); int enterAnimationId = windowAnimationStyle.getResourceId(com.android.internal.R .styleable.WindowAnimation_activityOpenRemoteViewsEnterAnimation, 0); windowStyle.recycle(); windowAnimationStyle.recycle(); if (enterAnimationId != 0) { opts = ActivityOptions.makeCustomAnimation(context, enterAnimationId, 0); opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } } if (opts == null && mViewIds != null && mElementNames != null) { View parent = (View) view.getParent(); while (parent != null && !(parent instanceof AppWidgetHostView)) { parent = (View) parent.getParent(); } if (parent instanceof AppWidgetHostView) { opts = ((AppWidgetHostView) parent).createSharedElementActivityOptions( mViewIds.toArray(), mElementNames.toArray(new String[mElementNames.size()]), intent); } } if (opts == null) { opts = ActivityOptions.makeBasic(); opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } if (view.getDisplay() != null) { opts.setLaunchDisplayId(view.getDisplay().getDisplayId()); } else { // TODO(b/218409359): Remove once bug is fixed. Log.w(LOG_TAG, "getLaunchOptions: view.getDisplay() is null!", new Exception()); } // If the user interacts with a visible element it is safe to assume they consent that // something is going to start. opts.setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); opts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); return Pair.create(intent, opts); } } /** @hide */ public static boolean startPendingIntent(View view, PendingIntent pendingIntent, Pair options) { try { // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? Context context = view.getContext(); // The NEW_TASK flags are applied through the activity options and not as a part of // the call to startIntentSender() to ensure that they are consistently applied to // both mutable and immutable PendingIntents. context.startIntentSender( pendingIntent.getIntentSender(), options.first, 0, 0, 0, options.second.toBundle()); } catch (IntentSender.SendIntentException e) { Log.e(LOG_TAG, "Cannot send pending intent: ", e); return false; } catch (Exception e) { Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e); return false; } return true; } /** Representation of a fixed list of items to be displayed in a RemoteViews collection. */ public static final class RemoteCollectionItems implements Parcelable { private final long[] mIds; private final RemoteViews[] mViews; private final boolean mHasStableIds; private final int mViewTypeCount; private HierarchyRootData mHierarchyRootData; RemoteCollectionItems( long[] ids, RemoteViews[] views, boolean hasStableIds, int viewTypeCount) { mIds = ids; mViews = views; mHasStableIds = hasStableIds; mViewTypeCount = viewTypeCount; if (ids.length != views.length) { throw new IllegalArgumentException( "RemoteCollectionItems has different number of ids and views"); } if (viewTypeCount < 1) { throw new IllegalArgumentException("View type count must be >= 1"); } int layoutIdCount = (int) Arrays.stream(views) .mapToInt(RemoteViews::getLayoutId) .distinct() .count(); if (layoutIdCount > viewTypeCount) { throw new IllegalArgumentException( "View type count is set to " + viewTypeCount + ", but the collection " + "contains " + layoutIdCount + " different layout ids"); } // Until the collection items are attached to a parent, we configure the first item // to be the root of the others to share caches and save space during serialization. if (views.length > 0) { setHierarchyRootData(views[0].getHierarchyRootData()); views[0].mIsRoot = true; } } RemoteCollectionItems(@NonNull Parcel in, @Nullable HierarchyRootData hierarchyRootData) { mHasStableIds = in.readBoolean(); mViewTypeCount = in.readInt(); int length = in.readInt(); mIds = new long[length]; in.readLongArray(mIds); boolean attached = in.readBoolean(); mViews = new RemoteViews[length]; int firstChildIndex; if (attached) { if (hierarchyRootData == null) { throw new IllegalStateException("Cannot unparcel a RemoteCollectionItems that " + "was parceled as attached without providing data for a root " + "RemoteViews"); } mHierarchyRootData = hierarchyRootData; firstChildIndex = 0; } else { mViews[0] = new RemoteViews(in); mHierarchyRootData = mViews[0].getHierarchyRootData(); firstChildIndex = 1; } for (int i = firstChildIndex; i < length; i++) { mViews[i] = new RemoteViews( in, mHierarchyRootData, /* info= */ null, /* depth= */ 0); } } void setHierarchyRootData(@NonNull HierarchyRootData rootData) { mHierarchyRootData = rootData; for (RemoteViews view : mViews) { view.configureAsChild(rootData); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { writeToParcel(dest, flags, /* attached= */ false); } private void writeToParcel(@NonNull Parcel dest, int flags, boolean attached) { boolean prevAllowSquashing = dest.allowSquashing(); dest.writeBoolean(mHasStableIds); dest.writeInt(mViewTypeCount); dest.writeInt(mIds.length); dest.writeLongArray(mIds); if (attached && mHierarchyRootData == null) { throw new IllegalStateException("Cannot call writeToParcelAttached for a " + "RemoteCollectionItems without first calling setHierarchyRootData()"); } // Write whether we parceled as attached or not. This allows cleaner validation and // proper error messaging when unparceling later. dest.writeBoolean(attached); boolean restoreRoot = false; if (!attached && mViews.length > 0 && !mViews[0].mIsRoot) { // If we're writing unattached, temporarily set the first item as the root so that // the bitmap cache is written to the parcel. restoreRoot = true; mViews[0].mIsRoot = true; } for (RemoteViews view : mViews) { view.writeToParcel(dest, flags); } if (restoreRoot) mViews[0].mIsRoot = false; dest.restoreAllowSquashing(prevAllowSquashing); } /** * Returns the id for {@code position}. See {@link #hasStableIds()} for whether this id * should be considered meaningful across collection updates. * * @return Id for the position. */ public long getItemId(int position) { return mIds[position]; } /** * Returns the {@link RemoteViews} to display at {@code position}. * * @return RemoteViews for the position. */ @NonNull public RemoteViews getItemView(int position) { return mViews[position]; } /** * Returns the number of elements in the collection. * * @return Count of items. */ public int getItemCount() { return mIds.length; } /** * Returns the view type count for the collection when used in an adapter * * @return Count of view types for the collection when used in an adapter. * @see android.widget.Adapter#getViewTypeCount() */ public int getViewTypeCount() { return mViewTypeCount; } /** * Indicates whether the item ids are stable across changes to the underlying data. * * @return True if the same id always refers to the same object. * @see android.widget.Adapter#hasStableIds() */ public boolean hasStableIds() { return mHasStableIds; } @NonNull public static final Creator CREATOR = new Creator() { @NonNull @Override public RemoteCollectionItems createFromParcel(@NonNull Parcel source) { return new RemoteCollectionItems(source, /* hierarchyRoot= */ null); } @NonNull @Override public RemoteCollectionItems[] newArray(int size) { return new RemoteCollectionItems[size]; } }; /** Builder class for {@link RemoteCollectionItems} objects.*/ public static final class Builder { private final LongArray mIds = new LongArray(); private final List mViews = new ArrayList<>(); private boolean mHasStableIds; private int mViewTypeCount; /** * Adds a {@link RemoteViews} to the collection. * * @param id Id to associate with the row. Use {@link #setHasStableIds(boolean)} to * indicate that ids are stable across changes to the collection. * @param view RemoteViews to display for the row. */ @NonNull // Covered by getItemId, getItemView, getItemCount. @SuppressLint("MissingGetterMatchingBuilder") public Builder addItem(long id, @NonNull RemoteViews view) { if (view == null) throw new NullPointerException(); if (view.hasMultipleLayouts()) { throw new IllegalArgumentException( "RemoteViews used in a RemoteCollectionItems cannot specify separate " + "layouts for orientations or sizes."); } mIds.add(id); mViews.add(view); return this; } /** * Sets whether the item ids are stable across changes to the underlying data. * * @see android.widget.Adapter#hasStableIds() */ @NonNull public Builder setHasStableIds(boolean hasStableIds) { mHasStableIds = hasStableIds; return this; } /** * Sets the view type count for the collection when used in an adapter. This can be set * to the maximum number of different layout ids that will be used by RemoteViews in * this collection. * * If this value is not set, then a value will be inferred from the provided items. As * a result, the adapter may need to be recreated when the list is updated with * previously unseen RemoteViews layouts for new items. * * @see android.widget.Adapter#getViewTypeCount() */ @NonNull public Builder setViewTypeCount(int viewTypeCount) { mViewTypeCount = viewTypeCount; return this; } /** Creates the {@link RemoteCollectionItems} defined by this builder. */ @NonNull public RemoteCollectionItems build() { if (mViewTypeCount < 1) { // If a view type count wasn't specified, set it to be the number of distinct // layout ids used in the items. mViewTypeCount = (int) mViews.stream() .mapToInt(RemoteViews::getLayoutId) .distinct() .count(); } return new RemoteCollectionItems( mIds.toArray(), mViews.toArray(new RemoteViews[0]), mHasStableIds, Math.max(mViewTypeCount, 1)); } } /** * See {@link RemoteViews#visitUris(Consumer)}. */ private void visitUris(@NonNull Consumer visitor) { for (RemoteViews view : mViews) { view.visitUris(visitor); } } } /** * A data parcel that carries the instructions to draw the RemoteViews, as an alternative to * XML layout. */ @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public static final class DrawInstructions { private static final long VERSION = 1L; @NonNull final List mInstructions; private DrawInstructions() { throw new UnsupportedOperationException( "DrawInstructions cannot be instantiate without instructions"); } private DrawInstructions(@NonNull List instructions) { // Create and retain an immutable copy of given instructions. mInstructions = new ArrayList<>(instructions.size()); for (byte[] instruction : instructions) { final int len = instruction.length; final byte[] target = new byte[len]; System.arraycopy(instruction, 0, target, 0, len); mInstructions.add(target); } } @Nullable private static DrawInstructions readFromParcel(@NonNull final Parcel in) { int size = in.readInt(); if (size == -1) { return null; } byte[] instruction; final List instructions = new ArrayList<>(size); for (int i = 0; i < size; i++) { instruction = in.readBlob(); instructions.add(instruction); } return new DrawInstructions(instructions); } private static void writeToParcel(@Nullable final DrawInstructions drawInstructions, @NonNull final Parcel dest, final int flags) { if (drawInstructions == null) { dest.writeInt(-1); return; } final List instructions = drawInstructions.mInstructions; dest.writeInt(instructions.size()); for (byte[] instruction : instructions) { dest.writeBlob(instruction); } } /** * Version number of {@link DrawInstructions} currently supported. */ @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public static long getSupportedVersion() { return VERSION; } /** * Builder class for {@link DrawInstructions} objects. */ @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public static final class Builder { private final List mInstructions; /** * Constructor. * * @param instructions Information to draw the RemoteViews. */ @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public Builder(@NonNull final List instructions) { mInstructions = new ArrayList<>(instructions); } /** * Creates a {@link DrawInstructions} instance. */ @NonNull @FlaggedApi(FLAG_DRAW_DATA_PARCEL) public DrawInstructions build() { return new DrawInstructions(mInstructions); } } } /** * Get the ID of the top-level view of the XML layout, if set using * {@link RemoteViews#RemoteViews(String, int, int)}. */ @IdRes public int getViewId() { return mViewId; } /** * Set the provider instance ID. * * This should only be used by {@link com.android.server.appwidget.AppWidgetService}. * @hide */ public void setProviderInstanceId(long id) { mProviderInstanceId = id; } /** * Get the provider instance id. * * This should uniquely identifies {@link RemoteViews} coming from a given App Widget * Provider. This changes each time the App Widget provider update the {@link RemoteViews} of * its widget. Returns -1 if the {@link RemoteViews} doesn't come from an App Widget provider. * @hide */ public long getProviderInstanceId() { return mProviderInstanceId; } /** * Identify the child of this {@link RemoteViews}, or 0 if this is not a child. * * The returned value is always a small integer, currently between 0 and 17. */ private int getChildId(@NonNull RemoteViews child) { if (child == this) { return 0; } if (hasSizedRemoteViews()) { for (int i = 0; i < mSizedRemoteViews.size(); i++) { if (mSizedRemoteViews.get(i) == child) { return i + 1; } } } if (hasLandscapeAndPortraitLayouts()) { if (mLandscape == child) { return 1; } else if (mPortrait == child) { return 2; } } // This is not a child of this RemoteViews. return 0; } /** * Identify uniquely this RemoteViews, or returns -1 if not possible. * * @param parent If the {@link RemoteViews} is not a root {@link RemoteViews}, this should be * the parent that contains it. * * @hide */ public long computeUniqueId(@Nullable RemoteViews parent) { if (mIsRoot) { long viewId = getProviderInstanceId(); if (viewId != -1) { viewId <<= 8; } return viewId; } if (parent == null) { return -1; } long viewId = parent.getProviderInstanceId(); if (viewId == -1) { return -1; } int childId = parent.getChildId(this); if (childId == -1) { return -1; } viewId <<= 8; viewId |= childId; return viewId; } @Nullable private static Pair getPackageUserKey(@Nullable ApplicationInfo info) { if (info == null || info.packageName == null) return null; return Pair.create(info.packageName, info.uid); } private HierarchyRootData getHierarchyRootData() { return new HierarchyRootData(mBitmapCache, mCollectionCache, mApplicationInfoCache, mClassCookies); } private static final class HierarchyRootData { final BitmapCache mBitmapCache; final RemoteCollectionCache mRemoteCollectionCache; final ApplicationInfoCache mApplicationInfoCache; final Map mClassCookies; HierarchyRootData( BitmapCache bitmapCache, RemoteCollectionCache remoteCollectionCache, ApplicationInfoCache applicationInfoCache, Map classCookies) { mBitmapCache = bitmapCache; mRemoteCollectionCache = remoteCollectionCache; mApplicationInfoCache = applicationInfoCache; mClassCookies = classCookies; } } }