| /* |
| * 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. |
| * |
| * <p>{@code RemoteViews} is limited to support for the following layouts:</p> |
| * <ul> |
| * <li>{@link android.widget.AdapterViewFlipper}</li> |
| * <li>{@link android.widget.FrameLayout}</li> |
| * <li>{@link android.widget.GridLayout}</li> |
| * <li>{@link android.widget.GridView}</li> |
| * <li>{@link android.widget.LinearLayout}</li> |
| * <li>{@link android.widget.ListView}</li> |
| * <li>{@link android.widget.RelativeLayout}</li> |
| * <li>{@link android.widget.StackView}</li> |
| * <li>{@link android.widget.ViewFlipper}</li> |
| * </ul> |
| * <p>And the following widgets:</p> |
| * <ul> |
| * <li>{@link android.widget.AnalogClock}</li> |
| * <li>{@link android.widget.Button}</li> |
| * <li>{@link android.widget.Chronometer}</li> |
| * <li>{@link android.widget.ImageButton}</li> |
| * <li>{@link android.widget.ImageView}</li> |
| * <li>{@link android.widget.ProgressBar}</li> |
| * <li>{@link android.widget.TextClock}</li> |
| * <li>{@link android.widget.TextView}</li> |
| * </ul> |
| * <p>As of API 31, the following widgets and layouts may also be used:</p> |
| * <ul> |
| * <li>{@link android.widget.CheckBox}</li> |
| * <li>{@link android.widget.RadioButton}</li> |
| * <li>{@link android.widget.RadioGroup}</li> |
| * <li>{@link android.widget.Switch}</li> |
| * </ul> |
| * <p>Descendants of these classes are not supported.</p> |
| */ |
| 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<Action> 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<RemoteViews> mSizedRemoteViews = null; |
| |
| /** |
| * Ideal size for this RemoteViews. |
| * |
| * Only to be used on children views used in a {@link RemoteViews} with |
| * {@link RemoteViews#hasSizedRemoteViews()}. |
| */ |
| private 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<Class, Object> 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<PendingIntent> mPendingIntentTemplate; |
| |
| @Nullable |
| private SparseArray<Intent> mFillInIntent; |
| |
| private static final InteractionHandler DEFAULT_INTERACTION_HANDLER = |
| (view, pendingIntent, response) -> |
| startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)); |
| |
| private static final ArrayMap<MethodKey, MethodArgs> 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<Bitmap> 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<Uri> 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<String, Action> map = new HashMap<String, Action>(); |
| if (mActions == null) { |
| mActions = new ArrayList<Action>(); |
| } |
| |
| int count = mActions.size(); |
| for (int i = 0; i < count; i++) { |
| Action a = mActions.get(i); |
| map.put(a.getUniqueKey(), a); |
| } |
| |
| ArrayList<Action> 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<Uri> 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<Uri> 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<View> 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<Uri> 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<Pair<String, Integer>, 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<String, Integer> 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<String, Integer> 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<String, Integer> 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<Uri> 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<Void> collectAllIntents() { |
| return mCollectionCache.collectAllIntentsNoComplete(this); |
| } |
| |
| private class RemoteCollectionCache { |
| private final SparseArray<String> mIdToUriMapping = new SparseArray<>(); |
| private final Map<String, RemoteCollectionItems> 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<Void> collectAllIntentsNoComplete( |
| @NonNull RemoteViews inViews) { |
| SparseArray<Intent> 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<Intent> 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<Void> connectAllUniqueIntents(int individualSize, |
| @NonNull SparseArray<Intent> idToIntentMapping) { |
| List<CompletableFuture<Void>> 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<RemoteCollectionItems> 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<RemoteCollectionItems> 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<Intent> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<? extends View> 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. |
| * <p> |
| * 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()}. |
| * <p> |
| */ |
| 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. |
| * <p> |
| * The operation will be performed on the {@link Drawable} returned by the |
| * target {@link View#getBackground()}. |
| * <p> |
| */ |
| 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<Bitmap> 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<Uri> 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<Uri> 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<Uri> 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. |
| * |
| * <p>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<Uri> 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<byte[]> 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<SizeF, RemoteViews> remoteViews) { |
| if (remoteViews.isEmpty()) { |
| throw new IllegalArgumentException("The set of RemoteViews cannot be empty"); |
| } |
| if (remoteViews.size() > MAX_INIT_VIEW_COUNT) { |
| throw new IllegalArgumentException("Too many RemoteViews in constructor"); |
| } |
| if (remoteViews.size() == 1) { |
| // 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<Class, Object> initializeSizedRemoteViews(Iterator<RemoteViews> remoteViews) { |
| List<RemoteViews> sizedRemoteViews = new ArrayList<>(); |
| Map<Class, Object> classCookies = null; |
| float viewArea = Float.MAX_VALUE; |
| RemoteViews smallestView = null; |
| while (remoteViews.hasNext()) { |
| RemoteViews view = remoteViews.next(); |
| 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> 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}. |
| * |
| * <p>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. |
| * <p> |
| * |
| * @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}. |
| * <p> |
| * |
| * @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<RemoteViews> 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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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)}. |
| * |
| * <p>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 |
| * <p class="note">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.</p> |
| * |
| * @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. |
| * |
| * <p><strong>Caller beware: this may throw</strong> |
| * |
| * @param context Default context to use |
| * @param parent Parent that the resulting view hierarchy will be attached to. This method |
| * does <strong>not</strong> 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 <strong>not</strong> 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<Void, Void, ViewTree> |
| 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. |
| * |
| * <p><strong>Caller beware: this may throw</strong> |
| * |
| * @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<PendingIntent> getPendingIntentTemplate() { |
| if (mPendingIntentTemplate == null) { |
| mPendingIntentTemplate = new SparseArray<>(); |
| } |
| return mPendingIntentTemplate; |
| } |
| |
| @NonNull |
| private SparseArray<Intent> 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<Intent> 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<RemoteViews> CREATOR = |
| new Parcelable.Creator<RemoteViews>() { |
| 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<ViewTree> 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 extends View> 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<View> 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<View> 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<String> 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<Uri> 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<Intent, ActivityOptions> 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<Intent, ActivityOptions> 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<RemoteCollectionItems> CREATOR = |
| new Creator<RemoteCollectionItems>() { |
| @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<RemoteViews> 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<Uri> 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<byte[]> mInstructions; |
| |
| private DrawInstructions() { |
| throw new UnsupportedOperationException( |
| "DrawInstructions cannot be instantiate without instructions"); |
| } |
| |
| private DrawInstructions(@NonNull List<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> mInstructions; |
| |
| /** |
| * Constructor. |
| * |
| * @param instructions Information to draw the RemoteViews. |
| */ |
| @FlaggedApi(FLAG_DRAW_DATA_PARCEL) |
| public Builder(@NonNull final List<byte[]> 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<String, Integer> 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<Class, Object> mClassCookies; |
| |
| HierarchyRootData( |
| BitmapCache bitmapCache, |
| RemoteCollectionCache remoteCollectionCache, |
| ApplicationInfoCache applicationInfoCache, |
| Map<Class, Object> classCookies) { |
| mBitmapCache = bitmapCache; |
| mRemoteCollectionCache = remoteCollectionCache; |
| mApplicationInfoCache = applicationInfoCache; |
| mClassCookies = classCookies; |
| } |
| } |
| } |