src.android.widget.RemoteViews Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* 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 android.annotation.AttrRes;
import android.annotation.ColorInt;
import android.annotation.ColorRes;
import android.annotation.DimenRes;
import android.annotation.DrawableRes;
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.PendingIntent;
import android.app.RemoteInput;
import android.appwidget.AppWidgetHostView;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentSender;
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.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.Process;
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.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.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.ContrastColorUtil;
import com.android.internal.util.Preconditions;
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.Stack;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* A class that describes a view hierarchy that can be displayed in
* another process. The hierarchy is inflated from a layout resource
* file, and this class provides some basic operations for modifying
* the content of the inflated hierarchy.
*
* {@code RemoteViews} is limited to support for the following layouts:
*
* - {@link android.widget.AdapterViewFlipper}
* - {@link android.widget.FrameLayout}
* - {@link android.widget.GridLayout}
* - {@link android.widget.GridView}
* - {@link android.widget.LinearLayout}
* - {@link android.widget.ListView}
* - {@link android.widget.RelativeLayout}
* - {@link android.widget.StackView}
* - {@link android.widget.ViewFlipper}
*
* And the following widgets:
*
* - {@link android.widget.AnalogClock}
* - {@link android.widget.Button}
* - {@link android.widget.Chronometer}
* - {@link android.widget.ImageButton}
* - {@link android.widget.ImageView}
* - {@link android.widget.ProgressBar}
* - {@link android.widget.TextClock}
* - {@link android.widget.TextView}
*
* As of API 31, the following widgets and layouts may also be used:
*
* - {@link android.widget.CheckBox}
* - {@link android.widget.RadioButton}
* - {@link android.widget.RadioGroup}
* - {@link android.widget.Switch}
*
* Descendants of these classes are not supported.
*/
public class RemoteViews implements Parcelable, Filter {
private static final String LOG_TAG = "RemoteViews";
/** The intent extra for whether the view whose checked state changed is currently checked. */
public static final String EXTRA_CHECKED = "android.widget.extra.CHECKED";
/**
* The intent extra that contains the appWidgetId.
* @hide
*/
static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId";
/**
* The intent extra that contains {@code true} if inflating as dak text theme.
* @hide
*/
static final String EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND = "remoteAdapterOnLightBackground";
/**
* The intent extra that contains the bounds for all shared elements.
*/
public static final String EXTRA_SHARED_ELEMENT_BOUNDS =
"android.widget.extra.SHARED_ELEMENT_BOUNDS";
/**
* Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and
* {@link #RemoteViews(RemoteViews, RemoteViews)}.
*/
private static final int MAX_NESTED_VIEWS = 10;
/**
* Maximum number of RemoteViews that can be specified in constructor.
*/
private static final int MAX_INIT_VIEW_COUNT = 16;
// The unique identifiers for each custom {@link Action}.
private static final int SET_ON_CLICK_RESPONSE_TAG = 1;
private static final int REFLECTION_ACTION_TAG = 2;
private static final int SET_DRAWABLE_TINT_TAG = 3;
private static final int VIEW_GROUP_ACTION_ADD_TAG = 4;
private static final int VIEW_CONTENT_NAVIGATION_TAG = 5;
private static final int SET_EMPTY_VIEW_ACTION_TAG = 6;
private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7;
private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8;
private static final int SET_REMOTE_VIEW_ADAPTER_INTENT_TAG = 10;
private static final int TEXT_VIEW_DRAWABLE_ACTION_TAG = 11;
private static final int BITMAP_REFLECTION_ACTION_TAG = 12;
private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13;
private static final int VIEW_PADDING_ACTION_TAG = 14;
private static final int SET_REMOTE_VIEW_ADAPTER_LIST_TAG = 15;
private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18;
private static final int LAYOUT_PARAM_ACTION_TAG = 19;
private static final int OVERRIDE_TEXT_COLORS_TAG = 20;
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;
/** @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;
/**
* 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);
/**
* Application that hosts the remote views.
*
* @hide
*/
@UnsupportedAppUsage
public ApplicationInfo mApplication;
/**
* The resource ID of the layout file. (Added to the parcel)
*/
@UnsupportedAppUsage
private int mLayoutId;
/**
* The resource ID of the layout file in dark text mode. (Added to the parcel)
*/
private int mLightBackgroundLayoutId = 0;
/**
* An array of actions to perform on the view tree once it has been
* inflated
*/
@UnsupportedAppUsage
private ArrayList mActions;
/**
* Maps bitmaps to unique indicies to avoid Bitmap duplication.
*/
@UnsupportedAppUsage
private BitmapCache mBitmapCache;
/**
* Indicates whether or not this RemoteViews object is contained as a child of any other
* RemoteViews.
*/
private boolean mIsRoot = true;
/**
* Constants to whether or not this RemoteViews is composed of a landscape and portrait
* RemoteViews.
*/
private static final int MODE_NORMAL = 0;
private static final int MODE_HAS_LANDSCAPE_AND_PORTRAIT = 1;
private static final int MODE_HAS_SIZED_REMOTEVIEWS = 2;
/**
* Used in conjunction with the special constructor
* {@link #RemoteViews(RemoteViews, RemoteViews)} to keep track of the landscape and portrait
* RemoteViews.
*/
private RemoteViews mLandscape = null;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private RemoteViews mPortrait = null;
/**
* List of RemoteViews with their ideal size. There must be at least two if the map is not null.
*
* The smallest remote view is always the last element in the list.
*/
private List mSizedRemoteViews = null;
/**
* Ideal size for this RemoteViews.
*
* Only to be used on children views used in a {@link RemoteViews} with
* {@link RemoteViews#hasSizedRemoteViews()}.
*/
private SizeF mIdealSize = null;
@ApplyFlags
private int mApplyFlags = 0;
/**
* Id to use to override the ID of the top-level view in this RemoteViews.
*
* Only used if this RemoteViews is defined from a XML layout value.
*/
private int mViewId = View.NO_ID;
/**
* Id used to uniquely identify a {@link RemoteViews} instance coming from a given provider.
*/
private long mProviderInstanceId = -1;
/** Class cookies of the Parcel this instance was read from. */
private Map mClassCookies;
private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
(view, pendingIntent, response) ->
startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
private static final ArrayMap sMethods = new ArrayMap<>();
/**
* This key is used to perform lookups in sMethods without causing allocations.
*/
private static final MethodKey sLookupKey = new MethodKey();
/**
* @hide
*/
public void setRemoteInputs(@IdRes int viewId, RemoteInput[] remoteInputs) {
mActions.add(new SetRemoteInputsAction(viewId, remoteInputs));
}
/**
* Reduces all images and ensures that they are all below the given sizes.
*
* @param maxWidth the maximum width allowed
* @param maxHeight the maximum height allowed
*
* @hide
*/
public void reduceImageSizes(int maxWidth, int maxHeight) {
ArrayList cache = mBitmapCache.mBitmaps;
for (int i = 0; i < cache.size(); i++) {
Bitmap bitmap = cache.get(i);
cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
}
}
/**
* Override all text colors in this layout and replace them by the given text color.
*
* @param textColor The color to use.
*
* @hide
*/
public void overrideTextColors(int textColor) {
addAction(new OverrideTextColorsAction(textColor));
}
/**
* 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;
}
/**
* @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 implements Parcelable {
public abstract void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) throws ActionException;
public static final int MERGE_REPLACE = 0;
public static final int MERGE_APPEND = 1;
public static final int MERGE_IGNORE = 2;
public int describeContents() {
return 0;
}
public void setBitmapCache(BitmapCache bitmapCache) {
// 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() + "_" + viewId);
}
/**
* 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,
InteractionHandler handler, ColorResources colorResources) {
return this;
}
public boolean prefersAsyncApply() {
return false;
}
/**
* Overridden by subclasses which have (or inherit) an ApplicationInfo instance
* as member variable
*/
public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
return true;
}
public void visitUris(@NonNull Consumer visitor) {
// Nothing to visit by default
}
@IdRes
@UnsupportedAppUsage
int viewId;
}
/**
* Action class used during async inflation of RemoteViews. Subclasses are not parcelable.
*/
private static abstract 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, InteractionHandler handler,
ColorResources colorResources) {
}
};
/**
* Merges the passed RemoteViews actions with this RemoteViews actions according to
* action-specific merge rules.
*
* @param newRv
*
* @hide
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void mergeRemoteViews(RemoteViews newRv) {
if (newRv == null) return;
// We first copy the new RemoteViews, as the process of merging modifies the way the actions
// reference the bitmap cache. We don't want to modify the object as it may need to
// be merged and applied multiple times.
RemoteViews copy = new RemoteViews(newRv);
HashMap map = new HashMap();
if (mActions == null) {
mActions = new ArrayList();
}
int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
map.put(a.getUniqueKey(), a);
}
ArrayList newActions = copy.mActions;
if (newActions == null) return;
count = newActions.size();
for (int i = 0; i < count; i++) {
Action a = newActions.get(i);
String key = newActions.get(i).getUniqueKey();
int mergeBehavior = newActions.get(i).mergeBehavior();
if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) {
mActions.remove(map.get(key));
map.remove(key);
}
// If the merge behavior is ignore, we don't bother keeping the extra action
if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) {
mActions.add(a);
}
}
// Because pruning can remove the need for bitmaps, we reconstruct the bitmap cache
mBitmapCache = new BitmapCache();
setBitmapCache(mBitmapCache);
}
/**
* 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.
*
* @hide
*/
public void visitUris(@NonNull Consumer visitor) {
if (mActions != null) {
for (int i = 0; i < mActions.size(); i++) {
mActions.get(i).visitUris(visitor);
}
}
}
private static void visitIconUri(Icon icon, @NonNull Consumer visitor) {
if (icon != null && (icon.getType() == Icon.TYPE_URI
|| icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
visitor.accept(icon.getUri());
}
}
private static class RemoteViewsContextWrapper extends ContextWrapper {
private final Context mContextForResources;
RemoteViewsContextWrapper(Context context, Context contextForResources) {
super(context);
mContextForResources = contextForResources;
}
@Override
public Resources getResources() {
return mContextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return mContextForResources.getTheme();
}
@Override
public String getPackageName() {
return mContextForResources.getPackageName();
}
@Override
public 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 class SetEmptyView extends Action {
int emptyViewId;
SetEmptyView(@IdRes int viewId, @IdRes int emptyViewId) {
this.viewId = viewId;
this.emptyViewId = emptyViewId;
}
SetEmptyView(Parcel in) {
this.viewId = in.readInt();
this.emptyViewId = in.readInt();
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(this.viewId);
out.writeInt(this.emptyViewId);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View view = root.findViewById(viewId);
if (!(view instanceof AdapterView>)) return;
AdapterView> adapterView = (AdapterView>) view;
final View emptyView = root.findViewById(emptyViewId);
if (emptyView == null) return;
adapterView.setEmptyView(emptyView);
}
@Override
public int getActionTag() {
return SET_EMPTY_VIEW_ACTION_TAG;
}
}
private class SetPendingIntentTemplate extends Action {
public SetPendingIntentTemplate(@IdRes int id, PendingIntent pendingIntentTemplate) {
this.viewId = id;
this.pendingIntentTemplate = pendingIntentTemplate;
}
public SetPendingIntentTemplate(Parcel parcel) {
viewId = parcel.readInt();
pendingIntentTemplate = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
PendingIntent.writePendingIntentOrNullToParcel(pendingIntentTemplate, dest);
}
@Override
public void apply(View root, ViewGroup rootParent, final InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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, handler);
}
};
av.setOnItemClickListener(listener);
av.setTag(pendingIntentTemplate);
} else {
Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" +
"an AdapterView (id: " + viewId + ")");
return;
}
}
@Nullable
private RemoteResponse findRemoteResponseTag(@Nullable View rootView) {
if (rootView == null) return null;
ArrayDeque viewsToCheck = new ArrayDeque<>();
viewsToCheck.addLast(rootView);
while (!viewsToCheck.isEmpty()) {
View view = viewsToCheck.removeFirst();
Object tag = view.getTag(R.id.fillInIntent);
if (tag instanceof RemoteResponse) return (RemoteResponse) tag;
if (!(view instanceof ViewGroup)) continue;
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
viewsToCheck.addLast(viewGroup.getChildAt(i));
}
}
return null;
}
@Override
public int getActionTag() {
return SET_PENDING_INTENT_TEMPLATE_TAG;
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
PendingIntent pendingIntentTemplate;
}
private class SetRemoteViewsAdapterList extends Action {
public SetRemoteViewsAdapterList(@IdRes int id, ArrayList list,
int viewTypeCount) {
this.viewId = id;
this.list = list;
this.viewTypeCount = viewTypeCount;
}
public SetRemoteViewsAdapterList(Parcel parcel) {
viewId = parcel.readInt();
viewTypeCount = parcel.readInt();
list = parcel.createTypedArrayList(RemoteViews.CREATOR);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(viewTypeCount);
dest.writeTypedList(list, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
if (target == null) return;
// Ensure that we are applying to an AppWidget root
if (!(rootParent instanceof AppWidgetHostView)) {
Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " +
"AppWidgets (root id: " + viewId + ")");
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 setRemoteViewsAdapter on a view which is not " +
"an AbsListView or AdapterViewAnimator (id: " + viewId + ")");
return;
}
if (target instanceof AbsListView) {
AbsListView v = (AbsListView) target;
Adapter a = v.getAdapter();
if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
((RemoteViewsListAdapter) a).setViewsList(list);
} else {
v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount,
colorResources));
}
} else if (target instanceof AdapterViewAnimator) {
AdapterViewAnimator v = (AdapterViewAnimator) target;
Adapter a = v.getAdapter();
if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
((RemoteViewsListAdapter) a).setViewsList(list);
} else {
v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount,
colorResources));
}
}
}
@Override
public int getActionTag() {
return SET_REMOTE_VIEW_ADAPTER_LIST_TAG;
}
int viewTypeCount;
ArrayList list;
}
private static class SetRemoteCollectionItemListAdapterAction extends Action {
private final RemoteCollectionItems mItems;
SetRemoteCollectionItemListAdapterAction(@IdRes int id, RemoteCollectionItems items) {
viewId = id;
mItems = items;
}
SetRemoteCollectionItemListAdapterAction(Parcel parcel) {
viewId = parcel.readInt();
mItems = parcel.readTypedObject(RemoteCollectionItems.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeTypedObject(mItems, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
View target = root.findViewById(viewId);
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: " + viewId + ")");
return;
}
if (!(target instanceof AdapterView)) {
Log.e(LOG_TAG, "Cannot call setRemoteAdapter on a view which is not "
+ "an AdapterView (id: " + viewId + ")");
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() >= mItems.getViewTypeCount()) {
try {
((RemoteCollectionItemsAdapter) adapter).setData(
mItems, handler, 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(mItems, handler, 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;
}
}
private class SetRemoteViewsAdapterIntent extends Action {
public SetRemoteViewsAdapterIntent(@IdRes int id, Intent intent) {
this.viewId = id;
this.intent = intent;
}
public SetRemoteViewsAdapterIntent(Parcel parcel) {
viewId = parcel.readInt();
intent = parcel.readTypedObject(Intent.CREATOR);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeTypedObject(intent, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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: " + viewId + ")");
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: " + viewId + ")");
return;
}
// Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent
// RemoteViewsService
AppWidgetHostView host = (AppWidgetHostView) rootParent;
intent.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(intent, isAsync);
v.setRemoteViewsInteractionHandler(handler);
} else if (target instanceof AdapterViewAnimator) {
AdapterViewAnimator v = (AdapterViewAnimator) target;
v.setRemoteViewsAdapter(intent, isAsync);
v.setRemoteViewsOnClickHandler(handler);
}
}
@Override
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
InteractionHandler handler, ColorResources colorResources) {
SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(viewId, intent);
copy.isAsync = true;
return copy;
}
@Override
public int getActionTag() {
return SET_REMOTE_VIEW_ADAPTER_INTENT_TAG;
}
Intent intent;
boolean isAsync = false;
}
/**
* Equivalent to calling
* {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
* to launch the provided {@link PendingIntent}.
*/
private class SetOnClickResponse extends Action {
SetOnClickResponse(@IdRes int id, RemoteResponse response) {
this.viewId = id;
this.mResponse = response;
}
SetOnClickResponse(Parcel parcel) {
viewId = parcel.readInt();
mResponse = new RemoteResponse();
mResponse.readFromParcel(parcel);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
mResponse.writeToParcel(dest, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, final InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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: " + viewId + ")");
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, handler));
}
@Override
public int getActionTag() {
return SET_ON_CLICK_RESPONSE_TAG;
}
final RemoteResponse mResponse;
}
/**
* 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.viewId = id;
this.mResponse = response;
}
SetOnCheckedChangeResponse(Parcel parcel) {
viewId = parcel.readInt();
mResponse = new RemoteResponse();
mResponse.readFromParcel(parcel);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
mResponse.writeToParcel(dest, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, final InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
if (target == null) return;
if (!(target instanceof CompoundButton)) {
Log.w(LOG_TAG, "setOnCheckedChange methods cannot be used on "
+ "non-CompoundButton child (id: " + viewId + ")");
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: " + viewId + ")");
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, 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;
}
}
/** @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 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.
*
* The operation will be performed on the {@link Drawable} returned by the
* target {@link View#getBackground()} by default. If targetBackground is false,
* we assume the target is an {@link ImageView} and try applying the operations
* to {@link ImageView#getDrawable()}.
*
*/
private class SetDrawableTint extends Action {
SetDrawableTint(@IdRes int id, boolean targetBackground,
@ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) {
this.viewId = id;
this.targetBackground = targetBackground;
this.colorFilter = colorFilter;
this.filterMode = mode;
}
SetDrawableTint(Parcel parcel) {
viewId = parcel.readInt();
targetBackground = parcel.readInt() != 0;
colorFilter = parcel.readInt();
filterMode = PorterDuff.intToMode(parcel.readInt());
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(targetBackground ? 1 : 0);
dest.writeInt(colorFilter);
dest.writeInt(PorterDuff.modeToInt(filterMode));
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
if (target == null) return;
// Pick the correct drawable to modify for this view
Drawable targetDrawable = null;
if (targetBackground) {
targetDrawable = target.getBackground();
} else if (target instanceof ImageView) {
ImageView imageView = (ImageView) target;
targetDrawable = imageView.getDrawable();
}
if (targetDrawable != null) {
targetDrawable.mutate().setColorFilter(colorFilter, filterMode);
}
}
@Override
public int getActionTag() {
return SET_DRAWABLE_TINT_TAG;
}
boolean targetBackground;
@ColorInt int colorFilter;
PorterDuff.Mode filterMode;
}
/**
* Equivalent to calling
* {@link RippleDrawable#setColor(ColorStateList)},
* on the {@link Drawable} of a given view.
*
* The operation will be performed on the {@link Drawable} returned by the
* target {@link View#getBackground()}.
*
*/
private class SetRippleDrawableColor extends Action {
ColorStateList mColorStateList;
SetRippleDrawableColor(@IdRes int id, ColorStateList colorStateList) {
this.viewId = id;
this.mColorStateList = colorStateList;
}
SetRippleDrawableColor(Parcel parcel) {
viewId = parcel.readInt();
mColorStateList = parcel.readParcelable(null);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeParcelable(mColorStateList, 0);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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;
}
}
private final class ViewContentNavigation extends Action {
final boolean mNext;
ViewContentNavigation(@IdRes int viewId, boolean next) {
this.viewId = viewId;
this.mNext = next;
}
ViewContentNavigation(Parcel in) {
this.viewId = in.readInt();
this.mNext = in.readBoolean();
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(this.viewId);
out.writeBoolean(this.mNext);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View view = root.findViewById(viewId);
if (view == null) return;
try {
getMethod(view,
mNext ? "showNext" : "showPrevious", null, false /* async */).invoke(view);
} catch (Throwable ex) {
throw new ActionException(ex);
}
}
public int mergeBehavior() {
return MERGE_IGNORE;
}
@Override
public int getActionTag() {
return VIEW_CONTENT_NAVIGATION_TAG;
}
}
private static class BitmapCache {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
ArrayList mBitmaps;
int mBitmapMemory = -1;
public BitmapCache() {
mBitmaps = new ArrayList<>();
}
public BitmapCache(Parcel source) {
mBitmaps = source.createTypedArrayList(Bitmap.CREATOR);
}
public int getBitmapId(Bitmap b) {
if (b == null) {
return -1;
} else {
if (mBitmaps.contains(b)) {
return mBitmaps.indexOf(b);
} else {
mBitmaps.add(b);
mBitmapMemory = -1;
return (mBitmaps.size() - 1);
}
}
}
@Nullable
public Bitmap getBitmapForId(int id) {
if (id == -1 || id >= mBitmaps.size()) {
return null;
} else {
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 bitmapId;
@UnsupportedAppUsage
Bitmap bitmap;
@UnsupportedAppUsage
String methodName;
BitmapReflectionAction(@IdRes int viewId, String methodName, Bitmap bitmap) {
this.bitmap = bitmap;
this.viewId = viewId;
this.methodName = methodName;
bitmapId = mBitmapCache.getBitmapId(bitmap);
}
BitmapReflectionAction(Parcel in) {
viewId = in.readInt();
methodName = in.readString8();
bitmapId = in.readInt();
bitmap = mBitmapCache.getBitmapForId(bitmapId);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeString8(methodName);
dest.writeInt(bitmapId);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
ReflectionAction ra = new ReflectionAction(viewId, methodName,
BaseReflectionAction.BITMAP,
bitmap);
ra.apply(root, rootParent, handler, colorResources);
}
@Override
public void setBitmapCache(BitmapCache bitmapCache) {
bitmapId = bitmapCache.getBitmapId(bitmap);
}
@Override
public int getActionTag() {
return BITMAP_REFLECTION_ACTION_TAG;
}
}
/**
* Base class for the reflection actions.
*/
private abstract 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 methodName;
int type;
BaseReflectionAction(@IdRes int viewId, String methodName, int type) {
this.viewId = viewId;
this.methodName = methodName;
this.type = type;
}
BaseReflectionAction(Parcel in) {
this.viewId = in.readInt();
this.methodName = in.readString8();
this.type = in.readInt();
//noinspection ConstantIfStatement
if (false) {
Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
+ " methodName=" + this.methodName + " type=" + this.type);
}
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt(this.viewId);
out.writeString8(this.methodName);
out.writeInt(this.type);
}
/**
* 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, InteractionHandler handler,
ColorResources colorResources) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class> param = getParameterType(this.type);
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
Object value = getParameterValue(view);
try {
getMethod(view, this.methodName, param, false /* async */).invoke(view, value);
} catch (Throwable ex) {
throw new ActionException(ex);
}
}
@Override
public final Action initActionAsync(ViewTree root, ViewGroup rootParent,
InteractionHandler handler, ColorResources colorResources) {
final View view = root.findViewById(viewId);
if (view == null) return ACTION_NOOP;
Class> param = getParameterType(this.type);
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
Object value = getParameterValue(view);
try {
MethodHandle method = getMethod(view, this.methodName, param, true /* async */);
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(viewId).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 (methodName.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.methodName + this.type;
}
@Override
public final boolean prefersAsyncApply() {
return this.type == URI || this.type == ICON;
}
@Override
public final void visitUris(@NonNull Consumer visitor) {
switch (this.type) {
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;
}
}
}
/** Class for the reflection actions. */
private final class ReflectionAction extends BaseReflectionAction {
@UnsupportedAppUsage
Object value;
ReflectionAction(@IdRes int viewId, String methodName, int type, Object value) {
super(viewId, methodName, type);
this.value = 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.type) {
case BOOLEAN:
this.value = in.readBoolean();
break;
case BYTE:
this.value = in.readByte();
break;
case SHORT:
this.value = (short) in.readInt();
break;
case INT:
this.value = in.readInt();
break;
case LONG:
this.value = in.readLong();
break;
case FLOAT:
this.value = in.readFloat();
break;
case DOUBLE:
this.value = in.readDouble();
break;
case CHAR:
this.value = (char) in.readInt();
break;
case STRING:
this.value = in.readString8();
break;
case CHAR_SEQUENCE:
this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
break;
case URI:
this.value = in.readTypedObject(Uri.CREATOR);
break;
case BITMAP:
this.value = in.readTypedObject(Bitmap.CREATOR);
break;
case BUNDLE:
this.value = in.readBundle();
break;
case INTENT:
this.value = in.readTypedObject(Intent.CREATOR);
break;
case COLOR_STATE_LIST:
this.value = in.readTypedObject(ColorStateList.CREATOR);
break;
case ICON:
this.value = in.readTypedObject(Icon.CREATOR);
break;
case BLEND_MODE:
this.value = 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.type) {
case BOOLEAN:
out.writeBoolean((Boolean) this.value);
break;
case BYTE:
out.writeByte((Byte) this.value);
break;
case SHORT:
out.writeInt((Short) this.value);
break;
case INT:
out.writeInt((Integer) this.value);
break;
case LONG:
out.writeLong((Long) this.value);
break;
case FLOAT:
out.writeFloat((Float) this.value);
break;
case DOUBLE:
out.writeDouble((Double) this.value);
break;
case CHAR:
out.writeInt((int) ((Character) this.value).charValue());
break;
case STRING:
out.writeString8((String) this.value);
break;
case CHAR_SEQUENCE:
TextUtils.writeToParcel((CharSequence) this.value, out, flags);
break;
case BUNDLE:
out.writeBundle((Bundle) this.value);
break;
case BLEND_MODE:
out.writeInt(BlendMode.toValue((BlendMode) this.value));
break;
case URI:
case BITMAP:
case INTENT:
case COLOR_STATE_LIST:
case ICON:
out.writeTypedObject((Parcelable) this.value, flags);
break;
default:
break;
}
}
@Nullable
@Override
protected Object getParameterValue(@Nullable View view) throws ActionException {
return this.value;
}
@Override
public int getActionTag() {
return REFLECTION_ACTION_TAG;
}
}
private 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.type) {
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.type);
}
case COLOR_RESOURCE:
switch (this.type) {
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.type);
}
case STRING_RESOURCE:
switch (this.type) {
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.type);
}
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 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.type) {
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.type);
}
case COLOR_RESOURCE:
switch (this.type) {
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.type);
}
case STRING_RESOURCE:
switch (this.type) {
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.type);
}
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 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.type) {
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.type);
}
} catch (ActionException ex) {
throw ex;
} catch (Throwable t) {
throw new ActionException(t);
}
}
@Override
public int getActionTag() {
return COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG;
}
}
private 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.type) {
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.type);
}
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
switch (this.type) {
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;
}
}
/**
* 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, InteractionHandler handler,
ColorResources colorResources) {
mRunnable.run();
}
}
private void configureRemoteViewsAsChild(RemoteViews rv) {
rv.setBitmapCache(mBitmapCache);
rv.setNotRoot();
}
void setNotRoot() {
mIsRoot = false;
}
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.viewId = viewId;
mNestedViews = nestedViews;
mIndex = index;
mStableId = stableId;
if (nestedViews != null) {
configureRemoteViewsAsChild(nestedViews);
}
}
ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info,
int depth, Map classCookies) {
viewId = parcel.readInt();
mIndex = parcel.readInt();
mStableId = parcel.readInt();
mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth, classCookies);
mNestedViews.addFlags(mApplyFlags);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(mIndex);
dest.writeInt(mStableId);
mNestedViews.writeToParcel(dest, flags);
}
@Override
public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
return mNestedViews.hasSameAppInfo(parentInfo);
}
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, InteractionHandler handler,
ColorResources colorResources) {
final Context context = root.getContext();
final ViewGroup target = root.findViewById(viewId);
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);
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.reapply(context, child, handler, null /* size */, colorResources,
false /* topLevel */);
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, handler, null /* size */,
colorResources);
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,
InteractionHandler handler, ColorResources colorResources) {
// In the async implementation, update the view tree so that subsequent calls to
// findViewById return the current view.
root.createTree();
ViewTree target = root.findViewTreeById(viewId);
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 */, handler, null /* size */,
colorResources,
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,
InteractionHandler handler, ColorResources colorResources)
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, handler, colorResources,
() -> targetVg.removeViews(nextChild,
recycledViewIndex - nextChild + 1));
}
}
// If we cannot recycle, simply add the view at the same available slot.
return insertNewView(context, target, handler, colorResources, () -> {});
}
private Action insertNewView(Context context, ViewTree target, InteractionHandler handler,
ColorResources colorResources, Runnable finalizeAction) {
ViewGroup targetVg = (ViewGroup) target.mRoot;
int nextChild = getNextRecyclableChild(targetVg);
final AsyncApplyTask task = mNestedViews.getInternalAsyncApplyTask(context, targetVg,
null /* listener */, handler, null /* size */, colorResources,
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, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
task.onPostExecute(tree);
finalizeAction.run();
targetVg.addView(task.mResult, insertIndex);
}
};
}
@Override
public void setBitmapCache(BitmapCache bitmapCache) {
mNestedViews.setBitmapCache(bitmapCache);
}
@Override
public int mergeBehavior() {
return MERGE_APPEND;
}
@Override
public boolean prefersAsyncApply() {
return mNestedViews.prefersAsyncApply();
}
@Override
public int getActionTag() {
return VIEW_GROUP_ACTION_ADD_TAG;
}
}
/**
* ViewGroup methods related to removing child views.
*/
private class ViewGroupActionRemove extends Action {
/**
* Id that indicates that all child views of the affected ViewGroup should be removed.
*
* Using -2 because the default id is -1. This avoids accidentally matching that.
*/
private static final int REMOVE_ALL_VIEWS_ID = -2;
private int mViewIdToKeep;
ViewGroupActionRemove(@IdRes int viewId) {
this(viewId, REMOVE_ALL_VIEWS_ID);
}
ViewGroupActionRemove(@IdRes int viewId, @IdRes int viewIdToKeep) {
this.viewId = viewId;
mViewIdToKeep = viewIdToKeep;
}
ViewGroupActionRemove(Parcel parcel) {
viewId = parcel.readInt();
mViewIdToKeep = parcel.readInt();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(mViewIdToKeep);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final ViewGroup target = root.findViewById(viewId);
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,
InteractionHandler handler, ColorResources colorResources) {
// In the async implementation, update the view tree so that subsequent calls to
// findViewById return the current view.
root.createTree();
ViewTree target = root.findViewTreeById(viewId);
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, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
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 class RemoveFromParentAction extends Action {
RemoveFromParentAction(@IdRes int viewId) {
this.viewId = viewId;
}
RemoveFromParentAction(Parcel parcel) {
viewId = parcel.readInt();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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,
InteractionHandler handler, ColorResources colorResources) {
// In the async implementation, update the view tree so that subsequent calls to
// findViewById return the correct view.
root.createTree();
ViewTree target = root.findViewTreeById(viewId);
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, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
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 class TextViewDrawableAction extends Action {
public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, @DrawableRes int d1,
@DrawableRes int d2, @DrawableRes int d3, @DrawableRes int d4) {
this.viewId = viewId;
this.isRelative = isRelative;
this.useIcons = false;
this.d1 = d1;
this.d2 = d2;
this.d3 = d3;
this.d4 = d4;
}
public TextViewDrawableAction(@IdRes int viewId, boolean isRelative,
Icon i1, Icon i2, Icon i3, Icon i4) {
this.viewId = viewId;
this.isRelative = isRelative;
this.useIcons = true;
this.i1 = i1;
this.i2 = i2;
this.i3 = i3;
this.i4 = i4;
}
public TextViewDrawableAction(Parcel parcel) {
viewId = parcel.readInt();
isRelative = (parcel.readInt() != 0);
useIcons = (parcel.readInt() != 0);
if (useIcons) {
i1 = parcel.readTypedObject(Icon.CREATOR);
i2 = parcel.readTypedObject(Icon.CREATOR);
i3 = parcel.readTypedObject(Icon.CREATOR);
i4 = parcel.readTypedObject(Icon.CREATOR);
} else {
d1 = parcel.readInt();
d2 = parcel.readInt();
d3 = parcel.readInt();
d4 = parcel.readInt();
}
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(isRelative ? 1 : 0);
dest.writeInt(useIcons ? 1 : 0);
if (useIcons) {
dest.writeTypedObject(i1, 0);
dest.writeTypedObject(i2, 0);
dest.writeTypedObject(i3, 0);
dest.writeTypedObject(i4, 0);
} else {
dest.writeInt(d1);
dest.writeInt(d2);
dest.writeInt(d3);
dest.writeInt(d4);
}
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final TextView target = root.findViewById(viewId);
if (target == null) return;
if (drawablesLoaded) {
if (isRelative) {
target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
} else {
target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
}
} else if (useIcons) {
final Context ctx = target.getContext();
final Drawable id1 = i1 == null ? null : i1.loadDrawable(ctx);
final Drawable id2 = i2 == null ? null : i2.loadDrawable(ctx);
final Drawable id3 = i3 == null ? null : i3.loadDrawable(ctx);
final Drawable id4 = i4 == null ? null : i4.loadDrawable(ctx);
if (isRelative) {
target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
} else {
target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
}
} else {
if (isRelative) {
target.setCompoundDrawablesRelativeWithIntrinsicBounds(d1, d2, d3, d4);
} else {
target.setCompoundDrawablesWithIntrinsicBounds(d1, d2, d3, d4);
}
}
}
@Override
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
InteractionHandler handler, ColorResources colorResources) {
final TextView target = root.findViewById(viewId);
if (target == null) return ACTION_NOOP;
TextViewDrawableAction copy = useIcons ?
new TextViewDrawableAction(viewId, isRelative, i1, i2, i3, i4) :
new TextViewDrawableAction(viewId, isRelative, d1, d2, d3, d4);
// Load the drawables on the background thread.
copy.drawablesLoaded = true;
final Context ctx = target.getContext();
if (useIcons) {
copy.id1 = i1 == null ? null : i1.loadDrawable(ctx);
copy.id2 = i2 == null ? null : i2.loadDrawable(ctx);
copy.id3 = i3 == null ? null : i3.loadDrawable(ctx);
copy.id4 = i4 == null ? null : i4.loadDrawable(ctx);
} else {
copy.id1 = d1 == 0 ? null : ctx.getDrawable(d1);
copy.id2 = d2 == 0 ? null : ctx.getDrawable(d2);
copy.id3 = d3 == 0 ? null : ctx.getDrawable(d3);
copy.id4 = d4 == 0 ? null : ctx.getDrawable(d4);
}
return copy;
}
@Override
public boolean prefersAsyncApply() {
return useIcons;
}
@Override
public int getActionTag() {
return TEXT_VIEW_DRAWABLE_ACTION_TAG;
}
@Override
public void visitUris(@NonNull Consumer visitor) {
if (useIcons) {
visitIconUri(i1, visitor);
visitIconUri(i2, visitor);
visitIconUri(i3, visitor);
visitIconUri(i4, visitor);
}
}
boolean isRelative = false;
boolean useIcons = false;
int d1, d2, d3, d4;
Icon i1, i2, i3, i4;
boolean drawablesLoaded = false;
Drawable id1, id2, id3, id4;
}
/**
* Helper action to set text size on a TextView in any supported units.
*/
private class TextViewSizeAction extends Action {
TextViewSizeAction(@IdRes int viewId, @ComplexDimensionUnit int units, float size) {
this.viewId = viewId;
this.units = units;
this.size = size;
}
TextViewSizeAction(Parcel parcel) {
viewId = parcel.readInt();
units = parcel.readInt();
size = parcel.readFloat();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(units);
dest.writeFloat(size);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final TextView target = root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}
@Override
public int getActionTag() {
return TEXT_VIEW_SIZE_ACTION_TAG;
}
int units;
float size;
}
/**
* Helper action to set padding on a View.
*/
private class ViewPaddingAction extends Action {
public ViewPaddingAction(@IdRes int viewId, @Px int left, @Px int top,
@Px int right, @Px int bottom) {
this.viewId = viewId;
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public ViewPaddingAction(Parcel parcel) {
viewId = parcel.readInt();
left = parcel.readInt();
top = parcel.readInt();
right = parcel.readInt();
bottom = parcel.readInt();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(left);
dest.writeInt(top);
dest.writeInt(right);
dest.writeInt(bottom);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
if (target == null) return;
target.setPadding(left, top, right, bottom);
}
@Override
public int getActionTag() {
return VIEW_PADDING_ACTION_TAG;
}
@Px int left, top, right, bottom;
}
/**
* 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.viewId = 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.viewId = viewId;
this.mProperty = property;
this.mValueType = valueType;
this.mValue = value;
}
public LayoutParamAction(Parcel parcel) {
viewId = parcel.readInt();
mProperty = parcel.readInt();
mValueType = parcel.readInt();
mValue = parcel.readInt();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(mProperty);
dest.writeInt(mValueType);
dest.writeInt(mValue);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
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 class SetRemoteInputsAction extends Action {
public SetRemoteInputsAction(@IdRes int viewId, RemoteInput[] remoteInputs) {
this.viewId = viewId;
this.remoteInputs = remoteInputs;
}
public SetRemoteInputsAction(Parcel parcel) {
viewId = parcel.readInt();
remoteInputs = parcel.createTypedArray(RemoteInput.CREATOR);
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeTypedArray(remoteInputs, flags);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
final View target = root.findViewById(viewId);
if (target == null) return;
target.setTagInternal(R.id.remote_input_tag, remoteInputs);
}
@Override
public int getActionTag() {
return SET_REMOTE_INPUTS_ACTION_TAG;
}
final Parcelable[] remoteInputs;
}
/**
* Helper action to override all textViewColors
*/
private class OverrideTextColorsAction extends Action {
private final int textColor;
public OverrideTextColorsAction(int textColor) {
this.textColor = textColor;
}
public OverrideTextColorsAction(Parcel parcel) {
textColor = parcel.readInt();
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(textColor);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) {
// Let's traverse the viewtree and override all textColors!
Stack viewsToProcess = new Stack<>();
viewsToProcess.add(root);
while (!viewsToProcess.isEmpty()) {
View v = viewsToProcess.pop();
if (v instanceof TextView) {
TextView textView = (TextView) v;
textView.setText(ContrastColorUtil.clearColorSpans(textView.getText()));
textView.setTextColor(textColor);
}
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
viewsToProcess.push(viewGroup.getChildAt(i));
}
}
}
}
@Override
public int getActionTag() {
return OVERRIDE_TEXT_COLORS_TAG;
}
}
private 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, InteractionHandler handler,
ColorResources colorResources) {
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.viewId = viewId;
mChecked = checked;
}
SetCompoundButtonCheckedAction(Parcel in) {
viewId = in.readInt();
mChecked = in.readBoolean();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeBoolean(mChecked);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources)
throws ActionException {
final View target = root.findViewById(viewId);
if (target == null) return;
if (!(target instanceof CompoundButton)) {
Log.w(LOG_TAG, "Cannot set checked to view "
+ viewId + " 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.viewId = viewId;
mCheckedId = checkedId;
}
SetRadioGroupCheckedAction(Parcel in) {
viewId = in.readInt();
mCheckedId = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(mCheckedId);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
final View target = root.findViewById(viewId);
if (target == null) return;
if (!(target instanceof RadioGroup)) {
Log.w(LOG_TAG, "Cannot check " + viewId + " 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.viewId = viewId;
this.mValueType = valueType;
this.mValue = value;
}
SetViewOutlinePreferredRadiusAction(
@IdRes int viewId, float radius, @ComplexDimensionUnit int units) {
this.viewId = viewId;
this.mValueType = VALUE_TYPE_COMPLEX_UNIT;
this.mValue = TypedValue.createComplexDimension(radius, units);
}
SetViewOutlinePreferredRadiusAction(Parcel in) {
viewId = in.readInt();
mValueType = in.readInt();
mValue = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
dest.writeInt(mValueType);
dest.writeInt(mValue);
}
@Override
public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
ColorResources colorResources) throws ActionException {
final View target = root.findViewById(viewId);
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);
}
}
/**
* 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;
mBitmapCache = new BitmapCache();
mClassCookies = null;
}
private boolean hasMultipleLayouts() {
return hasLandscapeAndPortraitLayouts() || hasSizedRemoteViews();
}
private boolean hasLandscapeAndPortraitLayouts() {
return (mLandscape != null) && (mPortrait != null);
}
private boolean hasSizedRemoteViews() {
return mSizedRemoteViews != null;
}
private @Nullable 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;
mBitmapCache = new BitmapCache();
configureRemoteViewsAsChild(landscape);
configureRemoteViewsAsChild(portrait);
mClassCookies = (portrait.mClassCookies != null)
? portrait.mClassCookies : landscape.mClassCookies;
}
/**
* Create a new RemoteViews object that will inflate the layout with the closest size
* specification.
*
* The default remote views in that case is always the one with the smallest area.
*
* If the {@link RemoteViews} host provides the size of the view, the layout with the largest
* area that fits entirely in the provided size will be used (i.e. the width and height of
* the layout must be less than the size of the view, with a 1dp margin to account for
* rounding). If no layout fits in the view, the layout with the smallest area will be used.
*
* @param remoteViews Mapping of size to layout.
* @throws IllegalArgumentException if the map is empty, there are more than
* MAX_INIT_VIEW_COUNT layouts or the remote views are not all from the same application.
*/
public RemoteViews(@NonNull Map remoteViews) {
if (remoteViews.isEmpty()) {
throw new IllegalArgumentException("The set of RemoteViews cannot be empty");
}
if (remoteViews.size() > MAX_INIT_VIEW_COUNT) {
throw new IllegalArgumentException("Too many RemoteViews in constructor");
}
if (remoteViews.size() == 1) {
initializeFrom(remoteViews.values().iterator().next());
return;
}
mBitmapCache = new BitmapCache();
mClassCookies = initializeSizedRemoteViews(
remoteViews.entrySet().stream().map(
entry -> {
entry.getValue().setIdealSize(entry.getKey());
return entry.getValue();
}
).iterator()
);
RemoteViews smallestView = findSmallestRemoteView();
mApplication = smallestView.mApplication;
mLayoutId = smallestView.mLayoutId;
mViewId = smallestView.mViewId;
mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId;
}
// Initialize mSizedRemoteViews and return the class cookies.
private Map initializeSizedRemoteViews(Iterator remoteViews) {
List sizedRemoteViews = new ArrayList<>();
Map classCookies = null;
float viewArea = Float.MAX_VALUE;
RemoteViews smallestView = null;
while (remoteViews.hasNext()) {
RemoteViews view = remoteViews.next();
SizeF size = view.getIdealSize();
if (size == null) {
throw new IllegalStateException("Expected RemoteViews to have ideal size");
}
float newViewArea = size.getWidth() * size.getHeight();
if (smallestView != null && !view.hasSameAppInfo(smallestView.mApplication)) {
throw new IllegalArgumentException(
"All RemoteViews must share the same package and user");
}
if (smallestView == null || newViewArea < viewArea) {
if (smallestView != null) {
sizedRemoteViews.add(smallestView);
}
viewArea = newViewArea;
smallestView = view;
} else {
sizedRemoteViews.add(view);
}
configureRemoteViewsAsChild(view);
view.setIdealSize(size);
if (classCookies == null) {
classCookies = view.mClassCookies;
}
}
sizedRemoteViews.add(smallestView);
mSizedRemoteViews = sizedRemoteViews;
return classCookies;
}
/**
* Creates a copy of another RemoteViews.
*/
public RemoteViews(RemoteViews src) {
initializeFrom(src);
}
private void initializeFrom(RemoteViews src) {
mBitmapCache = src.mBitmapCache;
mApplication = src.mApplication;
mIsRoot = src.mIsRoot;
mLayoutId = src.mLayoutId;
mLightBackgroundLayoutId = src.mLightBackgroundLayoutId;
mApplyFlags = src.mApplyFlags;
mClassCookies = src.mClassCookies;
mIdealSize = src.mIdealSize;
mProviderInstanceId = src.mProviderInstanceId;
if (src.hasLandscapeAndPortraitLayouts()) {
mLandscape = new RemoteViews(src.mLandscape);
mPortrait = new RemoteViews(src.mPortrait);
}
if (src.hasSizedRemoteViews()) {
mSizedRemoteViews = new ArrayList<>(src.mSizedRemoteViews.size());
for (RemoteViews srcView : src.mSizedRemoteViews) {
mSizedRemoteViews.add(new RemoteViews(srcView));
}
}
if (src.mActions != null) {
Parcel p = Parcel.obtain();
p.putClassCookies(mClassCookies);
src.writeActionsToParcel(p);
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, setting a new BitmapCache will
// re-initialize the cache.
setBitmapCache(new BitmapCache());
}
/**
* Reads a RemoteViews object from a parcel.
*
* @param parcel
*/
public RemoteViews(Parcel parcel) {
this(parcel, null, null, 0, null);
}
private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth,
Map classCookies) {
if (depth > MAX_NESTED_VIEWS
&& (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) {
throw new IllegalArgumentException("Too many nested views.");
}
depth++;
int mode = parcel.readInt();
// We only store a bitmap cache in the root of the RemoteViews.
if (bitmapCache == null) {
mBitmapCache = new BitmapCache(parcel);
// Store the class cookies such that they are available when we clone this RemoteView.
mClassCookies = parcel.copyClassCookies();
} else {
setBitmapCache(bitmapCache);
mClassCookies = classCookies;
setNotRoot();
}
if (mode == MODE_NORMAL) {
mApplication = parcel.readInt() == 0 ? info :
ApplicationInfo.CREATOR.createFromParcel(parcel);
mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel);
mLayoutId = parcel.readInt();
mViewId = parcel.readInt();
mLightBackgroundLayoutId = parcel.readInt();
readActionsFromParcel(parcel, depth);
} else if (mode == MODE_HAS_SIZED_REMOTEVIEWS) {
int numViews = parcel.readInt();
if (numViews > MAX_INIT_VIEW_COUNT) {
throw new IllegalArgumentException(
"Too many views in mapping from size to RemoteViews.");
}
List remoteViews = new ArrayList<>(numViews);
for (int i = 0; i < numViews; i++) {
RemoteViews view = new RemoteViews(parcel, mBitmapCache, info, depth,
mClassCookies);
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, mBitmapCache, info, depth, mClassCookies);
mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth,
mClassCookies);
mApplication = mPortrait.mApplication;
mLayoutId = mPortrait.mLayoutId;
mViewId = mPortrait.mViewId;
mLightBackgroundLayoutId = mPortrait.mLightBackgroundLayoutId;
}
mApplyFlags = parcel.readInt();
mProviderInstanceId = parcel.readLong();
}
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, mBitmapCache, mApplication, depth,
mClassCookies);
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_VIEW_ADAPTER_LIST_TAG:
return new SetRemoteViewsAdapterList(parcel);
case SET_REMOTE_INPUTS_ACTION_TAG:
return new SetRemoteInputsAction(parcel);
case LAYOUT_PARAM_ACTION_TAG:
return new LayoutParamAction(parcel);
case OVERRIDE_TEXT_COLORS_TAG:
return new OverrideTextColorsAction(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);
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;
}
/**
* Recursively sets BitmapCache in the hierarchy and update the bitmap ids.
*/
private void setBitmapCache(BitmapCache bitmapCache) {
mBitmapCache = bitmapCache;
if (hasSizedRemoteViews()) {
for (RemoteViews remoteView : mSizedRemoteViews) {
remoteView.setBitmapCache(bitmapCache);
}
} else if (hasLandscapeAndPortraitLayouts()) {
mLandscape.setBitmapCache(bitmapCache);
mPortrait.setBitmapCache(bitmapCache);
} else {
if (mActions != null) {
final int count = mActions.size();
for (int i = 0; i < count; ++i) {
mActions.get(i).setBitmapCache(bitmapCache);
}
}
}
}
/**
* 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()}
*/
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()}
*/
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) {
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) {
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)));
}
/**
* @hide
* Equivalent to calling
* {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
* on the {@link Drawable} of a given view.
*
*
* @param viewId The id of the view that contains the target
* {@link Drawable}
* @param targetBackground If true, apply these parameters to the
* {@link Drawable} returned by
* {@link android.view.View#getBackground()}. Otherwise, assume
* the target view is an {@link ImageView} and apply them to
* {@link ImageView#getDrawable()}.
* @param colorFilter Specify a color for a
* {@link android.graphics.ColorFilter} for this drawable. This will be ignored if
* {@code mode} is {@code null}.
* @param mode Specify a PorterDuff mode for this drawable, or null to leave
* unchanged.
*/
public void setDrawableTint(@IdRes int viewId, boolean targetBackground,
@ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) {
addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode));
}
/**
* @hide
* Equivalent to calling
* {@link RippleDrawable#setColor(ColorStateList)} on the {@link Drawable} of a given view,
* assuming it's a {@link RippleDrawable}.
*
*
* @param viewId The id of the view that contains the target
* {@link RippleDrawable}
* @param colorStateList Specify a color for a
* {@link ColorStateList} for this drawable.
*/
public void setRippleDrawableColor(@IdRes int viewId, ColorStateList colorStateList) {
addAction(new SetRippleDrawableColor(viewId, colorStateList));
}
/**
* @hide
* Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}.
*
* @param viewId The id of the view whose tint should change
* @param tint the tint to apply, may be {@code null} to clear tint
*/
public void setProgressTintList(@IdRes int viewId, ColorStateList tint) {
addAction(new ReflectionAction(viewId, "setProgressTintList",
BaseReflectionAction.COLOR_STATE_LIST, tint));
}
/**
* @hide
* Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}.
*
* @param viewId The id of the view whose tint should change
* @param tint the tint to apply, may be {@code null} to clear tint
*/
public void setProgressBackgroundTintList(@IdRes int viewId, ColorStateList tint) {
addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList",
BaseReflectionAction.COLOR_STATE_LIST, tint));
}
/**
* @hide
* Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}.
*
* @param viewId The id of the view whose tint should change
* @param tint the tint to apply, may be {@code null} to clear tint
*/
public void setProgressIndeterminateTintList(@IdRes int viewId, ColorStateList tint) {
addAction(new ReflectionAction(viewId, "setIndeterminateTintList",
BaseReflectionAction.COLOR_STATE_LIST, tint));
}
/**
* Equivalent to calling {@link android.widget.TextView#setTextColor(int)}.
*
* @param viewId The id of the view whose text color should change
* @param color Sets the text color for all the states (normal, selected,
* focused) to be this color.
*/
public void setTextColor(@IdRes int viewId, @ColorInt int color) {
setInt(viewId, "setTextColor", color);
}
/**
* @hide
* Equivalent to calling {@link android.widget.TextView#setTextColor(ColorStateList)}.
*
* @param viewId The id of the view whose text color should change
* @param colors the text colors to set
*/
public void setTextColor(@IdRes int viewId, ColorStateList colors) {
addAction(new ReflectionAction(viewId, "setTextColor",
BaseReflectionAction.COLOR_STATE_LIST, colors));
}
/**
* Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
*
* @param appWidgetId The id of the app widget which contains the specified view. (This
* parameter is ignored in this deprecated method)
* @param viewId The id of the {@link AdapterView}
* @param intent The intent of the service which will be
* providing data to the RemoteViewsAdapter
* @deprecated This method has been deprecated. See
* {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)}
*/
@Deprecated
public void setRemoteAdapter(int appWidgetId, @IdRes int viewId, Intent intent) {
setRemoteAdapter(viewId, intent);
}
/**
* Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
* Can only be used for App Widgets.
*
* @param viewId The id of the {@link AdapterView}
* @param intent The intent of the service which will be
* providing data to the RemoteViewsAdapter
*/
public void setRemoteAdapter(@IdRes int viewId, Intent intent) {
addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
}
/**
* Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
* ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
* This is a simpler but less flexible approach to populating collection widgets. Its use is
* encouraged for most scenarios, as long as the total memory within the list of RemoteViews
* is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
* RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
* possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
*
* This API is supported in the compatibility library for previous API levels, see
* RemoteViewsCompat.
*
* @param viewId The id of the {@link AdapterView}
* @param list The list of RemoteViews which will populate the view specified by viewId.
* @param viewTypeCount The maximum number of unique layout id's used to construct the list of
* RemoteViews. This count cannot change during the life-cycle of a given widget, so this
* parameter should account for the maximum possible number of types that may appear in the
* See {@link Adapter#getViewTypeCount()}.
*
* @hide
* @deprecated this appears to have no users outside of UnsupportedAppUsage?
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@Deprecated
public void setRemoteAdapter(@IdRes int viewId, ArrayList list,
int viewTypeCount) {
addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount));
}
/**
* Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
* ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
* This is a simpler but less flexible approach to populating collection widgets. Its use is
* encouraged for most scenarios, as long as the total memory within the list of RemoteViews
* is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
* RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
* possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
*
* This API is supported in the compatibility library for previous API levels, see
* RemoteViewsCompat.
*
* @param viewId The id of the {@link AdapterView}.
* @param items The items to display in the {@link AdapterView}.
*/
public void setRemoteAdapter(@IdRes int viewId, @NonNull RemoteCollectionItems items) {
addAction(new SetRemoteCollectionItemListAdapterAction(viewId, items));
}
/**
* Equivalent to calling {@link ListView#smoothScrollToPosition(int)}.
*
* @param viewId The id of the view to change
* @param position Scroll to this adapter position
*/
public void setScrollPosition(@IdRes int viewId, int position) {
setInt(viewId, "smoothScrollToPosition", position);
}
/**
* Equivalent to calling {@link ListView#smoothScrollByOffset(int)}.
*
* @param viewId The id of the view to change
* @param offset Scroll by this adapter position offset
*/
public void setRelativeScrollPosition(@IdRes int viewId, int offset) {
setInt(viewId, "smoothScrollByOffset", offset);
}
/**
* Equivalent to calling {@link android.view.View#setPadding(int, int, int, int)}.
*
* @param viewId The id of the view to change
* @param left the left padding in pixels
* @param top the top padding in pixels
* @param right the right padding in pixels
* @param bottom the bottom padding in pixels
*/
public void setViewPadding(@IdRes int viewId,
@Px int left, @Px int top, @Px int right, @Px int bottom) {
addAction(new ViewPaddingAction(viewId, left, top, right, bottom));
}
/**
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
* Only works if the {@link View#getLayoutParams()} supports margins.
*
* @param viewId The id of the view to change
* @param type The margin being set e.g. {@link #MARGIN_END}
* @param dimen a dimension resource to apply to the margin, or 0 to clear the margin.
*/
public void setViewLayoutMarginDimen(@IdRes int viewId, @MarginType int type,
@DimenRes int dimen) {
addAction(new LayoutParamAction(viewId, type, dimen, VALUE_TYPE_RESOURCE));
}
/**
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
* Only works if the {@link View#getLayoutParams()} supports margins.
*
* @param viewId The id of the view to change
* @param type The margin being set e.g. {@link #MARGIN_END}
* @param attr a dimension attribute to apply to the margin, or 0 to clear the margin.
*/
public void setViewLayoutMarginAttr(@IdRes int viewId, @MarginType int type,
@AttrRes int attr) {
addAction(new LayoutParamAction(viewId, type, attr, VALUE_TYPE_ATTRIBUTE));
}
/**
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
* Only works if the {@link View#getLayoutParams()} supports margins.
*
* NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0.
* Setting margins in pixels will behave poorly when the RemoteViews object is used on a
* display with a different density.
*
* @param viewId The id of the view to change
* @param type The margin being set e.g. {@link #MARGIN_END}
* @param value a value for the margin the given units.
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
*/
public void setViewLayoutMargin(@IdRes int viewId, @MarginType int type, float value,
@ComplexDimensionUnit int units) {
addAction(new LayoutParamAction(viewId, type, value, units));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} except that you may
* provide the value in any dimension units.
*
*
NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0,
* {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}.
* Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a
* display with a different density.
*
* @param width Width of the view in the given units
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
*/
public void setViewLayoutWidth(@IdRes int viewId, float width,
@ComplexDimensionUnit int units) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, width, units));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with
* the result of {@link Resources#getDimensionPixelSize(int)}.
*
* @param widthDimen the dimension resource for the view's width
*/
public void setViewLayoutWidthDimen(@IdRes int viewId, @DimenRes int widthDimen) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthDimen,
VALUE_TYPE_RESOURCE));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with
* the value of the given attribute in the current theme.
*
* @param widthAttr the dimension attribute for the view's width
*/
public void setViewLayoutWidthAttr(@IdRes int viewId, @AttrRes int widthAttr) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthAttr,
VALUE_TYPE_ATTRIBUTE));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} except that you may
* provide the value in any dimension units.
*
*
NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0,
* {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}.
* Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a
* display with a different density.
*
* @param height height of the view in the given units
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
*/
public void setViewLayoutHeight(@IdRes int viewId, float height,
@ComplexDimensionUnit int units) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, height, units));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with
* the result of {@link Resources#getDimensionPixelSize(int)}.
*
* @param heightDimen a dimen resource to read the height from.
*/
public void setViewLayoutHeightDimen(@IdRes int viewId, @DimenRes int heightDimen) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightDimen,
VALUE_TYPE_RESOURCE));
}
/**
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with
* the value of the given attribute in the current theme.
*
* @param heightAttr a dimen attribute to read the height from.
*/
public void setViewLayoutHeightAttr(@IdRes int viewId, @AttrRes int heightAttr) {
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightAttr,
VALUE_TYPE_ATTRIBUTE));
}
/**
* Sets an OutlineProvider on the view whose corner radius is a dimension calculated using
* {@link TypedValue#applyDimension(int, float, DisplayMetrics)}.
*
*
NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0.
* Setting margins in pixels will behave poorly when the RemoteViews object is used on a
* display with a different density.
*/
public void setViewOutlinePreferredRadius(
@IdRes int viewId, float radius, @ComplexDimensionUnit int units) {
addAction(new SetViewOutlinePreferredRadiusAction(viewId, radius, units));
}
/**
* Sets an OutlineProvider on the view whose corner radius is a dimension resource with
* {@code resId}.
*/
public void setViewOutlinePreferredRadiusDimen(@IdRes int viewId, @DimenRes int resId) {
addAction(new SetViewOutlinePreferredRadiusAction(viewId, resId, VALUE_TYPE_RESOURCE));
}
/**
* Sets an OutlineProvider on the view whose corner radius is a dimension attribute with
* {@code attrId}.
*/
public void setViewOutlinePreferredRadiusAttr(@IdRes int viewId, @AttrRes int attrId) {
addAction(new SetViewOutlinePreferredRadiusAction(viewId, attrId, VALUE_TYPE_ATTRIBUTE));
}
/**
* Call a method taking one boolean on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setBoolean(@IdRes int viewId, String methodName, boolean value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BOOLEAN, value));
}
/**
* Call a method taking one byte on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setByte(@IdRes int viewId, String methodName, byte value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BYTE, value));
}
/**
* Call a method taking one short on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setShort(@IdRes int viewId, String methodName, short value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.SHORT, value));
}
/**
* Call a method taking one int on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setInt(@IdRes int viewId, String methodName, int value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
}
/**
* Call a method taking one int, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
* (re-)applied.
*
* Undefined resources will result in an exception, except 0 which will resolve to 0.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param dimenResource The resource to resolve and pass as argument to the method.
*/
public void setIntDimen(@IdRes int viewId, @NonNull String methodName,
@DimenRes int dimenResource) {
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
ResourceReflectionAction.DIMEN_RESOURCE, dimenResource));
}
/**
* Call a method taking one int, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the specified dimension at the time of inflation.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value of the dimension.
* @param unit The unit in which the value is specified.
*/
public void setIntDimen(@IdRes int viewId, @NonNull String methodName,
float value, @ComplexDimensionUnit int unit) {
addAction(new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.INT,
value, unit));
}
/**
* Call a method taking one int, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the theme attribute at the time the
* {@link RemoteViews} is (re-)applied.
*
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param dimenAttr The attribute to resolve and pass as argument to the method.
*/
public void setIntDimenAttr(@IdRes int viewId, @NonNull String methodName,
@AttrRes int dimenAttr) {
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT,
ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr));
}
/**
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
*
* The Color will be resolved from the resources at the time the {@link RemoteViews} is (re-)
* applied.
*
* Undefined resources will result in an exception, except 0 which will resolve to 0.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param colorResource The resource to resolve and pass as argument to the method.
*/
public void setColor(@IdRes int viewId, @NonNull String methodName,
@ColorRes int colorResource) {
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
ResourceReflectionAction.COLOR_RESOURCE, colorResource));
}
/**
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
*
* The Color will be resolved from the theme attribute at the time the {@link RemoteViews} is
* (re-)applied.
*
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param colorAttribute The theme attribute to resolve and pass as argument to the method.
*/
public void setColorAttr(@IdRes int viewId, @NonNull String methodName,
@AttrRes int colorAttribute) {
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT,
AttributeReflectionAction.COLOR_RESOURCE, colorAttribute));
}
/**
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param notNight The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_NO}
* @param night The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_YES}
*/
public void setColorInt(
@IdRes int viewId,
@NonNull String methodName,
@ColorInt int notNight,
@ColorInt int night) {
addAction(
new NightModeReflectionAction(
viewId,
methodName,
BaseReflectionAction.INT,
notNight,
night));
}
/**
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setColorStateList(@IdRes int viewId, @NonNull String methodName,
@Nullable ColorStateList value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST,
value));
}
/**
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param notNight The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_NO}
* @param night The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_YES}
*/
public void setColorStateList(
@IdRes int viewId,
@NonNull String methodName,
@Nullable ColorStateList notNight,
@Nullable ColorStateList night) {
addAction(
new NightModeReflectionAction(
viewId,
methodName,
BaseReflectionAction.COLOR_STATE_LIST,
notNight,
night));
}
/**
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
*
* The ColorStateList will be resolved from the resources at the time the {@link RemoteViews} is
* (re-)applied.
*
* Undefined resources will result in an exception, except 0 which will resolve to null.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param colorResource The resource to resolve and pass as argument to the method.
*/
public void setColorStateList(@IdRes int viewId, @NonNull String methodName,
@ColorRes int colorResource) {
addAction(new ResourceReflectionAction(viewId, methodName,
BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE,
colorResource));
}
/**
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
*
* The ColorStateList will be resolved from the theme attribute at the time the
* {@link RemoteViews} is (re-)applied.
*
* Unresolvable attributes will result in an exception, except 0 which will resolve to null.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param colorAttr The theme attribute to resolve and pass as argument to the method.
*/
public void setColorStateListAttr(@IdRes int viewId, @NonNull String methodName,
@AttrRes int colorAttr) {
addAction(new AttributeReflectionAction(viewId, methodName,
BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE,
colorAttr));
}
/**
* Call a method taking one long on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setLong(@IdRes int viewId, String methodName, long value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.LONG, value));
}
/**
* Call a method taking one float on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setFloat(@IdRes int viewId, String methodName, float value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT, value));
}
/**
* Call a method taking one float, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
* (re-)applied.
*
* Undefined resources will result in an exception, except 0 which will resolve to 0f.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param dimenResource The resource to resolve and pass as argument to the method.
*/
public void setFloatDimen(@IdRes int viewId, @NonNull String methodName,
@DimenRes int dimenResource) {
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT,
ResourceReflectionAction.DIMEN_RESOURCE, dimenResource));
}
/**
* Call a method taking one float, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
* (re-)applied.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value of the dimension.
* @param unit The unit in which the value is specified.
*/
public void setFloatDimen(@IdRes int viewId, @NonNull String methodName,
float value, @ComplexDimensionUnit int unit) {
addAction(
new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.FLOAT,
value, unit));
}
/**
* Call a method taking one float, a size in pixels, on a view in the layout for this
* RemoteViews.
*
* The dimension will be resolved from the theme attribute at the time the {@link RemoteViews}
* is (re-)applied.
*
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0f.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param dimenAttr The attribute to resolve and pass as argument to the method.
*/
public void setFloatDimenAttr(@IdRes int viewId, @NonNull String methodName,
@AttrRes int dimenAttr) {
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT,
ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr));
}
/**
* Call a method taking one double on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setDouble(@IdRes int viewId, String methodName, double value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.DOUBLE, value));
}
/**
* Call a method taking one char on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setChar(@IdRes int viewId, String methodName, char value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR, value));
}
/**
* Call a method taking one String on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setString(@IdRes int viewId, String methodName, String value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.STRING, value));
}
/**
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setCharSequence(@IdRes int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE,
value));
}
/**
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
*
* The CharSequence will be resolved from the resources at the time the {@link RemoteViews} is
* (re-)applied.
*
* Undefined resources will result in an exception, except 0 which will resolve to null.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param stringResource The resource to resolve and pass as argument to the method.
*/
public void setCharSequence(@IdRes int viewId, @NonNull String methodName,
@StringRes int stringResource) {
addAction(
new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE,
ResourceReflectionAction.STRING_RESOURCE, stringResource));
}
/**
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
*
* The CharSequence will be resolved from the theme attribute at the time the
* {@link RemoteViews} is (re-)applied.
*
* Unresolvable attributes will result in an exception, except 0 which will resolve to null.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param stringAttribute The attribute to resolve and pass as argument to the method.
*/
public void setCharSequenceAttr(@IdRes int viewId, @NonNull String methodName,
@AttrRes int stringAttribute) {
addAction(
new AttributeReflectionAction(viewId, methodName,
BaseReflectionAction.CHAR_SEQUENCE,
AttributeReflectionAction.STRING_RESOURCE, stringAttribute));
}
/**
* Call a method taking one Uri on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setUri(@IdRes int viewId, String methodName, Uri value) {
if (value != null) {
// Resolve any filesystem path before sending remotely
value = value.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
value.checkFileUriExposed("RemoteViews.setUri()");
}
}
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.URI, value));
}
/**
* Call a method taking one Bitmap on a view in the layout for this RemoteViews.
* @more
*
The bitmap will be flattened into the parcel if this object is
* sent across processes, so it may end up using a lot of memory, and may be fairly slow.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setBitmap(@IdRes int viewId, String methodName, Bitmap value) {
addAction(new BitmapReflectionAction(viewId, methodName, value));
}
/**
* Call a method taking one BlendMode on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setBlendMode(@IdRes int viewId, @NonNull String methodName,
@Nullable BlendMode value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BLEND_MODE, value));
}
/**
* Call a method taking one Bundle on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setBundle(@IdRes int viewId, String methodName, Bundle value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BUNDLE, value));
}
/**
* Call a method taking one Intent on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The {@link android.content.Intent} to pass the method.
*/
public void setIntent(@IdRes int viewId, String methodName, Intent value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INTENT, value));
}
/**
* Call a method taking one Icon on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The {@link android.graphics.drawable.Icon} to pass the method.
*/
public void setIcon(@IdRes int viewId, String methodName, Icon value) {
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.ICON, value));
}
/**
* Call a method taking one Icon on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param notNight The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_NO}
* @param night The value to pass to the method when the view's configuration is set to
* {@link Configuration#UI_MODE_NIGHT_YES}
*/
public void setIcon(
@IdRes int viewId,
@NonNull String methodName,
@Nullable Icon notNight,
@Nullable Icon night) {
addAction(
new NightModeReflectionAction(
viewId,
methodName,
BaseReflectionAction.ICON,
notNight,
night));
}
/**
* Equivalent to calling View.setContentDescription(CharSequence).
*
* @param viewId The id of the view whose content description should change.
* @param contentDescription The new content description for the view.
*/
public void setContentDescription(@IdRes int viewId, CharSequence contentDescription) {
setCharSequence(viewId, "setContentDescription", contentDescription);
}
/**
* Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}.
*
* @param viewId The id of the view whose before view in accessibility traversal to set.
* @param nextId The id of the next in the accessibility traversal.
**/
public void setAccessibilityTraversalBefore(@IdRes int viewId, @IdRes int nextId) {
setInt(viewId, "setAccessibilityTraversalBefore", nextId);
}
/**
* Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}.
*
* @param viewId The id of the view whose after view in accessibility traversal to set.
* @param nextId The id of the next in the accessibility traversal.
**/
public void setAccessibilityTraversalAfter(@IdRes int viewId, @IdRes int nextId) {
setInt(viewId, "setAccessibilityTraversalAfter", nextId);
}
/**
* Equivalent to calling {@link View#setLabelFor(int)}.
*
* @param viewId The id of the view whose property to set.
* @param labeledId The id of a view for which this view serves as a label.
*/
public void setLabelFor(@IdRes int viewId, @IdRes int labeledId) {
setInt(viewId, "setLabelFor", labeledId);
}
/**
* Equivalent to calling {@link android.widget.CompoundButton#setChecked(boolean)}.
*
* @param viewId The id of the view whose property to set.
* @param checked true to check the button, false to uncheck it.
*/
public void setCompoundButtonChecked(@IdRes int viewId, boolean checked) {
addAction(new SetCompoundButtonCheckedAction(viewId, checked));
}
/**
* Equivalent to calling {@link android.widget.RadioGroup#check(int)}.
*
* @param viewId The id of the view whose property to set.
* @param checkedId The unique id of the radio button to select in the group.
*/
public void setRadioGroupChecked(@IdRes int viewId, @IdRes int checkedId) {
addAction(new SetRadioGroupCheckedAction(viewId, checkedId));
}
/**
* Provides an alternate layout ID, which can be used to inflate this view. This layout will be
* used by the host when the widgets displayed on a light-background where foreground elements
* and text can safely draw using a dark color without any additional background protection.
*/
public void setLightBackgroundLayoutId(@LayoutRes int layoutId) {
mLightBackgroundLayoutId = layoutId;
}
/**
* If this view supports dark text versions, creates a copy representing that version,
* otherwise returns itself.
* @hide
*/
public RemoteViews getDarkTextViews() {
if (hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT)) {
return this;
}
try {
addFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT);
return new RemoteViews(this);
} finally {
mApplyFlags &= ~FLAG_USE_LIGHT_BACKGROUND_LAYOUT;
}
}
private RemoteViews getRemoteViewsToApply(Context context) {
if (hasLandscapeAndPortraitLayouts()) {
int orientation = context.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return mLandscape;
}
return mPortrait;
}
if (hasSizedRemoteViews()) {
return findSmallestRemoteView();
}
return this;
}
/**
* Returns the square distance between two points.
*
* This is particularly useful when we only care about the ordering of the distances.
*/
private static float squareDistance(SizeF p1, SizeF p2) {
float dx = p1.getWidth() - p2.getWidth();
float dy = p1.getHeight() - p2.getHeight();
return dx * dx + dy * dy;
}
/**
* Returns whether the layout fits in the space available to the widget.
*
* A layout fits on a widget if the widget size is known (i.e. not null) and both dimensions
* are smaller than the ones of the widget, adding some padding to account for rounding errors.
*/
private static boolean fitsIn(SizeF sizeLayout, @Nullable SizeF sizeWidget) {
return sizeWidget != null && (Math.ceil(sizeWidget.getWidth()) + 1 > sizeLayout.getWidth())
&& (Math.ceil(sizeWidget.getHeight()) + 1 > sizeLayout.getHeight());
}
private RemoteViews findBestFitLayout(@NonNull SizeF widgetSize) {
// Find the better remote view
RemoteViews bestFit = null;
float bestSqDist = Float.MAX_VALUE;
for (RemoteViews layout : mSizedRemoteViews) {
SizeF layoutSize = layout.getIdealSize();
if (layoutSize == null) {
throw new IllegalStateException("Expected RemoteViews to have ideal size");
}
if (fitsIn(layoutSize, widgetSize)) {
if (bestFit == null) {
bestFit = layout;
bestSqDist = squareDistance(layoutSize, widgetSize);
} else {
float newSqDist = squareDistance(layoutSize, widgetSize);
if (newSqDist < bestSqDist) {
bestFit = layout;
bestSqDist = newSqDist;
}
}
}
}
if (bestFit == null) {
Log.w(LOG_TAG, "Could not find a RemoteViews fitting the current size: " + widgetSize);
return findSmallestRemoteView();
}
return bestFit;
}
/**
* Returns the most appropriate {@link RemoteViews} given the context and, if not null, the
* size of the widget.
*
* If {@link RemoteViews#hasSizedRemoteViews()} returns true, the most appropriate view is
* the one that fits in the widget (according to {@link RemoteViews#fitsIn}) and has the
* diagonal the most similar to the widget. If no layout fits or the size of the widget is
* not specified, the one with the smallest area will be chosen.
*
* @hide
*/
public RemoteViews getRemoteViewsToApply(@NonNull Context context,
@Nullable SizeF widgetSize) {
if (!hasSizedRemoteViews() || widgetSize == null) {
// If there isn't multiple remote views, fall back on the previous methods.
return getRemoteViewsToApply(context);
}
return findBestFitLayout(widgetSize);
}
/**
* Checks whether the change of size will lead to using a different {@link RemoteViews}.
*
* @hide
*/
@Nullable
public RemoteViews getRemoteViewsToApplyIfDifferent(@Nullable SizeF oldSize,
@NonNull SizeF newSize) {
if (!hasSizedRemoteViews()) {
return null;
}
RemoteViews oldBestFit = oldSize == null ? findSmallestRemoteView() : findBestFitLayout(
oldSize);
RemoteViews newBestFit = findBestFitLayout(newSize);
if (oldBestFit != newBestFit) {
return newBestFit;
}
return null;
}
/**
* Inflates the view hierarchy represented by this object and applies
* all of the actions.
*
* Caller beware: this may throw
*
* @param context Default context to use
* @param parent Parent that the resulting view hierarchy will be attached to. This method
* does not attach the hierarchy. The caller should do so when appropriate.
* @return The inflated view hierarchy
*/
public View apply(Context context, ViewGroup parent) {
return apply(context, parent, null);
}
/** @hide */
public View apply(Context context, ViewGroup parent, InteractionHandler handler) {
return apply(context, parent, handler, null);
}
/** @hide */
public View apply(@NonNull Context context, @NonNull ViewGroup parent,
@Nullable InteractionHandler handler, @Nullable SizeF size) {
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
View result = inflateView(context, rvToApply, parent);
rvToApply.performApply(result, parent, handler, null);
return result;
}
/** @hide */
public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent,
@Nullable InteractionHandler handler, @StyleRes int applyThemeResId) {
return applyWithTheme(context, parent, handler, applyThemeResId, null);
}
/** @hide */
public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent,
@Nullable InteractionHandler handler, @StyleRes int applyThemeResId,
@Nullable SizeF size) {
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
View result = inflateView(context, rvToApply, parent, applyThemeResId, null);
rvToApply.performApply(result, parent, handler, null);
return result;
}
/** @hide */
public View apply(Context context, ViewGroup parent, InteractionHandler handler,
@Nullable SizeF size, @Nullable ColorResources colorResources) {
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
View result = inflateView(context, rvToApply, parent, 0, colorResources);
rvToApply.performApply(result, parent, handler, colorResources);
return result;
}
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
return inflateView(context, rv, parent, 0, null);
}
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 = getContextForResources(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);
}
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);
View v = inflater.inflate(rv.getLayoutId(), parent, false);
if (mViewId != View.NO_ID) {
v.setId(mViewId);
v.setTagInternal(R.id.remote_views_override_id, mViewId);
}
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
return v;
}
/**
* A static filter is much lighter than RemoteViews itself. It's optimized here only for
* RemoteVies class. Subclasses should always override this and return true if not overriding
* {@link this#onLoadClass(Class)}.
*
* @hide
*/
protected boolean shouldUseStaticFilter() {
return this.getClass().equals(RemoteViews.class);
}
/**
* Implement this interface to receive a callback when
* {@link #applyAsync} or {@link #reapplyAsync} is finished.
* @hide
*/
public interface OnViewAppliedListener {
/**
* Callback when the RemoteView has finished inflating,
* but no actions have been applied yet.
*/
default void onViewInflated(View v) {};
void onViewApplied(View v);
void onError(Exception e);
}
/**
* Applies the views asynchronously, moving as much of the task on the background
* thread as possible.
*
* @see #apply(Context, ViewGroup)
* @param context Default context to use
* @param parent Parent that the resulting view hierarchy will be attached to. This method
* does not attach the hierarchy. The caller should do so when appropriate.
* @param listener the callback to run when all actions have been applied. May be null.
* @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used.
* @return CancellationSignal
* @hide
*/
public CancellationSignal applyAsync(
Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) {
return applyAsync(context, parent, executor, listener, null /* handler */);
}
/** @hide */
public CancellationSignal applyAsync(Context context, ViewGroup parent,
Executor executor, OnViewAppliedListener listener, InteractionHandler handler) {
return applyAsync(context, parent, executor, listener, handler, null /* size */);
}
/** @hide */
public CancellationSignal applyAsync(Context context, ViewGroup parent,
Executor executor, OnViewAppliedListener listener, InteractionHandler handler,
SizeF size) {
return applyAsync(context, parent, executor, listener, handler, size,
null /* themeColors */);
}
/** @hide */
public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor,
OnViewAppliedListener listener, InteractionHandler handler, SizeF size,
ColorResources colorResources) {
return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener,
handler, colorResources, null /* result */,
true /* topLevel */).startTaskOnExecutor(executor);
}
private AsyncApplyTask getInternalAsyncApplyTask(Context context, ViewGroup parent,
OnViewAppliedListener listener, InteractionHandler handler, SizeF size,
ColorResources colorResources, View result) {
return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener,
handler, colorResources, result, false /* topLevel */);
}
private class AsyncApplyTask extends AsyncTask
implements CancellationSignal.OnCancelListener {
final CancellationSignal mCancelSignal = new CancellationSignal();
final RemoteViews mRV;
final ViewGroup mParent;
final Context mContext;
final OnViewAppliedListener mListener;
final InteractionHandler mHandler;
final ColorResources mColorResources;
/**
* 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,
InteractionHandler handler, ColorResources colorResources,
View result, boolean topLevel) {
mRV = rv;
mParent = parent;
mContext = context;
mListener = listener;
mColorResources = colorResources;
mHandler = handler;
mTopLevel = topLevel;
mResult = result;
}
@Nullable
@Override
protected ViewTree doInBackground(Void... params) {
try {
if (mResult == null) {
mResult = inflateView(mContext, mRV, mParent, 0, mColorResources);
}
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, mHandler,
mColorResources);
}
} 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) {
InteractionHandler handler = mHandler == null
? DEFAULT_INTERACTION_HANDLER : mHandler;
for (Action a : mActions) {
a.apply(viewTree.mRoot, mParent, handler, mColorResources);
}
}
// If the parent of the view is has is a root, resolve the recycling.
if (mTopLevel && mResult instanceof ViewGroup) {
finalizeViewRecycling((ViewGroup) mResult);
}
} catch (Exception e) {
mError = e;
}
}
if (mListener != null) {
if (mError != null) {
mListener.onError(mError);
} else {
mListener.onViewApplied(viewTree.mRoot);
}
} else if (mError != null) {
if (mError instanceof ActionException) {
throw (ActionException) mError;
} else {
throw new ActionException(mError);
}
}
}
@Override
public void onCancel() {
cancel(true);
}
private CancellationSignal startTaskOnExecutor(Executor executor) {
mCancelSignal.setOnCancelListener(this);
executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor);
return mCancelSignal;
}
}
/**
* Applies all of the actions to the provided view.
*
* Caller beware: this may throw
*
* @param v The view to apply the actions to. This should be the result of
* the {@link #apply(Context,ViewGroup)} call.
*/
public void reapply(Context context, View v) {
reapply(context, v, null /* handler */);
}
/** @hide */
public void reapply(Context context, View v, InteractionHandler handler) {
reapply(context, v, handler, null /* size */, null /* colorResources */);
}
/** @hide */
public void reapply(Context context, View v, InteractionHandler handler, SizeF size,
ColorResources colorResources) {
reapply(context, v, handler, size, colorResources, true);
}
/** @hide */
public boolean canRecycleView(@Nullable View v) {
if (v == null) {
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;
}
// 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, InteractionHandler handler, SizeF size,
ColorResources colorResources, boolean topLevel) {
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
// In the case that a view has this RemoteViews applied in one orientation or size, is
// persisted across change, and has the RemoteViews re-applied in a different situation
// (orientation or size), we throw an exception, since the layouts may be completely
// unrelated.
if (hasMultipleLayouts()) {
if (!rvToApply.canRecycleView(v)) {
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
" that does not share the same root layout id.");
}
}
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler, colorResources);
// If the parent of the view is has is a root, resolve the recycling.
if (topLevel && v instanceof ViewGroup) {
finalizeViewRecycling((ViewGroup) v);
}
}
/**
* 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 = getRemoteViewsToApply(context, size);
// In the case that a view has this RemoteViews applied in one orientation, is persisted
// across orientation change, and has the RemoteViews re-applied in the new orientation,
// we throw an exception, since the layouts may be completely unrelated.
if (hasMultipleLayouts()) {
if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
" that does not share the same root layout id.");
}
}
return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
context, listener, handler, colorResources, v, true /* topLevel */)
.startTaskOnExecutor(executor);
}
private void performApply(View v, ViewGroup parent, InteractionHandler handler,
ColorResources colorResources) {
if (mActions != null) {
handler = handler == null ? DEFAULT_INTERACTION_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler, colorResources);
}
}
}
/**
* 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) {
if (mApplication != null && mApplication.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.
mApplication = info;
}
if (hasSizedRemoteViews()) {
for (RemoteViews layout : mSizedRemoteViews) {
layout.updateAppInfo(info);
}
}
if (hasLandscapeAndPortraitLayouts()) {
mLandscape.updateAppInfo(info);
mPortrait.updateAppInfo(info);
}
}
private Context getContextForResources(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
&& context.getPackageName().equals(mApplication.packageName)) {
return context;
}
try {
return context.createApplicationContext(mApplication,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
}
}
return context;
}
/**
* 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 ResourcesLoader mLoader;
private ColorResources(ResourcesLoader loader) {
mLoader = loader;
}
/**
* 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);
}
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);
}
}
} 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;
}
public void writeToParcel(Parcel dest, int flags) {
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);
}
if (!mIsRoot && (flags & PARCELABLE_ELIDE_DUPLICATES) != 0) {
dest.writeInt(0);
} else {
dest.writeInt(1);
mApplication.writeToParcel(dest, flags);
}
if (mIsRoot || mIdealSize == null) {
dest.writeInt(0);
} else {
dest.writeInt(1);
mIdealSize.writeToParcel(dest, flags);
}
dest.writeInt(mLayoutId);
dest.writeInt(mViewId);
dest.writeInt(mLightBackgroundLayoutId);
writeActionsToParcel(dest);
} else if (hasSizedRemoteViews()) {
dest.writeInt(MODE_HAS_SIZED_REMOTEVIEWS);
if (mIsRoot) {
mBitmapCache.writeBitmapsToParcel(dest, flags);
}
int childFlags = flags;
dest.writeInt(mSizedRemoteViews.size());
for (RemoteViews view : mSizedRemoteViews) {
view.writeToParcel(dest, childFlags);
childFlags |= PARCELABLE_ELIDE_DUPLICATES;
}
} else {
dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT);
// We only write the bitmap cache if we are the root RemoteViews, as this cache
// is shared by all children.
if (mIsRoot) {
mBitmapCache.writeBitmapsToParcel(dest, flags);
}
mLandscape.writeToParcel(dest, flags);
// Both RemoteViews already share the same package and user
mPortrait.writeToParcel(dest, flags | PARCELABLE_ELIDE_DUPLICATES);
}
dest.writeInt(mApplyFlags);
dest.writeLong(mProviderInstanceId);
}
private void writeActionsToParcel(Parcel parcel) {
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, a.hasSameAppInfo(mApplication)
? PARCELABLE_ELIDE_DUPLICATES : 0);
}
}
@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.packageName.equals(info.packageName) && mApplication.uid == info.uid;
}
/**
* Parcelable.Creator that instantiates RemoteViews objects
*/
public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() {
public RemoteViews createFromParcel(Parcel parcel) {
return new RemoteViews(parcel);
}
public RemoteViews[] newArray(int size) {
return new RemoteViews[size];
}
};
/**
* A representation of the view hierarchy. Only views which have a valid ID are added
* and can be searched.
*/
private static class ViewTree {
private static final int INSERT_AT_END_INDEX = -1;
private View mRoot;
private ArrayList mChildren;
private ViewTree(View root) {
mRoot = root;
}
public void createTree() {
if (mChildren != null) {
return;
}
mChildren = new ArrayList<>();
if (mRoot instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) mRoot;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
addViewChild(vg.getChildAt(i));
}
}
}
@Nullable
public ViewTree findViewTreeById(@IdRes int id) {
if (mRoot.getId() == id) {
return this;
}
if (mChildren == null) {
return null;
}
for (ViewTree tree : mChildren) {
ViewTree result = tree.findViewTreeById(id);
if (result != null) {
return result;
}
}
return null;
}
@Nullable
public ViewTree findViewTreeParentOf(ViewTree child) {
if (mChildren == null) {
return null;
}
for (ViewTree tree : mChildren) {
if (tree == child) {
return this;
}
ViewTree result = tree.findViewTreeParentOf(child);
if (result != null) {
return result;
}
}
return null;
}
public void replaceView(View v) {
mRoot = v;
mChildren = null;
createTree();
}
@Nullable
public T findViewById(@IdRes int id) {
if (mChildren == null) {
return mRoot.findViewById(id);
}
ViewTree tree = findViewTreeById(id);
return tree == null ? null : (T) tree.mRoot;
}
public void addChild(ViewTree child) {
addChild(child, INSERT_AT_END_INDEX);
}
/**
* Adds the given {@link ViewTree} as a child at the given index.
*
* @param index The position at which to add the child or -1 to add last.
*/
public void addChild(ViewTree child, int index) {
if (mChildren == null) {
mChildren = new ArrayList<>();
}
child.createTree();
if (index == INSERT_AT_END_INDEX) {
mChildren.add(child);
return;
}
mChildren.add(index, child);
}
public void removeChildren(int start, int count) {
if (mChildren != null) {
for (int i = 0; i < count; i++) {
mChildren.remove(start);
}
}
}
private void addViewChild(View v) {
// ViewTree only contains Views which can be found using findViewById.
// If isRootNamespace is true, this view is skipped.
// @see ViewGroup#findViewTraversal(int)
if (v.isRootNamespace()) {
return;
}
final ViewTree target;
// If the view has a valid id, i.e., if can be found using findViewById, add it to the
// tree, otherwise skip this view and add its children instead.
if (v.getId() != 0) {
ViewTree tree = new ViewTree(v);
mChildren.add(tree);
target = tree;
} else {
target = this;
}
if (v instanceof ViewGroup) {
if (target.mChildren == null) {
target.mChildren = new ArrayList<>();
ViewGroup vg = (ViewGroup) v;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
target.addViewChild(vg.getChildAt(i));
}
}
}
}
/** Find the first child for which the condition is true and return its index. */
public int findChildIndex(Predicate condition) {
return findChildIndex(0, condition);
}
/**
* Find the first child, starting at {@code startIndex}, for which the condition is true and
* return its index.
*/
public int findChildIndex(int startIndex, Predicate condition) {
if (mChildren == null) {
return -1;
}
for (int i = startIndex; i < mChildren.size(); i++) {
if (condition.test(mChildren.get(i).mRoot)) {
return i;
}
}
return -1;
}
}
/**
* Class representing a response to an action performed on any element of a RemoteViews.
*/
public static class RemoteResponse {
/** @hide **/
@IntDef(prefix = "INTERACTION_TYPE_", value = {
INTERACTION_TYPE_CLICK,
INTERACTION_TYPE_CHECKED_CHANGE,
})
@Retention(RetentionPolicy.SOURCE)
@interface InteractionType {}
/** @hide */
public static final int INTERACTION_TYPE_CLICK = 0;
/** @hide */
public static final int INTERACTION_TYPE_CHECKED_CHANGE = 1;
private PendingIntent mPendingIntent;
private Intent mFillIntent;
private int mInteractionType = INTERACTION_TYPE_CLICK;
private IntArray mViewIds;
private ArrayList mElementNames;
/**
* Creates a response which sends a pending intent as part of the response. The source
* bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the
* target view in screen space.
* Note that any activity options associated with the mPendingIntent may get overridden
* before starting the intent.
*
* @param pendingIntent The {@link PendingIntent} to send as part of the response
*/
@NonNull
public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) {
RemoteResponse response = new RemoteResponse();
response.mPendingIntent = pendingIntent;
return response;
}
/**
* When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is
* very costly to set PendingIntents on the individual items, and is hence not recommended.
* Instead a single PendingIntent template can be set on the collection, see {@link
* RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click
* action of a given item can be distinguished by setting a fillInIntent on that item. The
* fillInIntent is then combined with the PendingIntent template in order to determine the
* final intent which will be executed when the item is clicked. This works as follows: any
* fields which are left blank in the PendingIntent template, but are provided by the
* fillInIntent will be overwritten, and the resulting PendingIntent will be used. The rest
* of the PendingIntent template will then be filled in with the associated fields that are
* set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details.
* Creates a response which sends a pending intent as part of the response. The source
* bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the
* target view in screen space.
* Note that any activity options associated with the mPendingIntent may get overridden
* before starting the intent.
*
* @param fillIntent The intent which will be combined with the parent's PendingIntent in
* order to determine the behavior of the response
* @see RemoteViews#setPendingIntentTemplate(int, PendingIntent)
* @see RemoteViews#setOnClickFillInIntent(int, Intent)
*/
@NonNull
public static RemoteResponse fromFillInIntent(@NonNull Intent fillIntent) {
RemoteResponse response = new RemoteResponse();
response.mFillIntent = fillIntent;
return response;
}
/**
* 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);
if (mPendingIntent == null) {
// Only write the intent if pending intent is 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);
if (mPendingIntent == null) {
mFillIntent = parcel.readTypedObject(Intent.CREATOR);
}
mInteractionType = parcel.readInt();
int[] viewIds = parcel.createIntArray();
mViewIds = viewIds == null ? null : IntArray.wrap(viewIds);
mElementNames = parcel.createStringArrayList();
}
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 RemoteViewsFrameLayout, 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 RemoteViewsAdapter.RemoteViewsFrameLayout))) {
parent = (View) parent.getParent();
}
return parent instanceof AdapterView> ? (AdapterView>) parent : null;
}
/** @hide */
public Pair getLaunchOptions(View view) {
Intent intent = mPendingIntent != 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);
}
return Pair.create(intent, opts);
}
}
/** @hide */
public static boolean startPendingIntent(View view, PendingIntent pendingIntent,
Pair options) {
try {
// TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
Context context = view.getContext();
// The NEW_TASK flags are applied through the activity options and not as a part of
// the call to startIntentSender() to ensure that they are consistently applied to
// both mutable and immutable PendingIntents.
context.startIntentSender(
pendingIntent.getIntentSender(), options.first,
0, 0, 0, options.second.toBundle());
} catch (IntentSender.SendIntentException e) {
Log.e(LOG_TAG, "Cannot send pending intent: ", e);
return false;
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e);
return false;
}
return true;
}
/** Representation of a fixed list of items to be displayed in a RemoteViews collection. */
public static final class RemoteCollectionItems implements Parcelable {
private final long[] mIds;
private final RemoteViews[] mViews;
private final boolean mHasStableIds;
private final int mViewTypeCount;
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");
}
}
RemoteCollectionItems(Parcel in) {
int length = in.readInt();
mIds = new long[length];
in.readLongArray(mIds);
mViews = new RemoteViews[length];
in.readTypedArray(mViews, RemoteViews.CREATOR);
mHasStableIds = in.readBoolean();
mViewTypeCount = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mIds.length);
dest.writeLongArray(mIds);
dest.writeTypedArray(mViews, flags);
dest.writeBoolean(mHasStableIds);
dest.writeInt(mViewTypeCount);
}
/**
* Returns the id for {@code position}. See {@link #hasStableIds()} for whether this id
* should be considered meaningful across collection updates.
*
* @return Id for the position.
*/
public long getItemId(int position) {
return mIds[position];
}
/**
* Returns the {@link RemoteViews} to display at {@code position}.
*
* @return RemoteViews for the position.
*/
@NonNull
public RemoteViews getItemView(int position) {
return mViews[position];
}
/**
* Returns the number of elements in the collection.
*
* @return Count of items.
*/
public int getItemCount() {
return mIds.length;
}
/**
* Returns the view type count for the collection when used in an adapter
*
* @return Count of view types for the collection when used in an adapter.
* @see android.widget.Adapter#getViewTypeCount()
*/
public int getViewTypeCount() {
return mViewTypeCount;
}
/**
* Indicates whether the item ids are stable across changes to the underlying data.
*
* @return True if the same id always refers to the same object.
* @see android.widget.Adapter#hasStableIds()
*/
public boolean hasStableIds() {
return mHasStableIds;
}
@NonNull
public static final Creator CREATOR =
new Creator() {
@NonNull
@Override
public RemoteCollectionItems createFromParcel(@NonNull Parcel source) {
return new RemoteCollectionItems(source);
}
@NonNull
@Override
public RemoteCollectionItems[] newArray(int size) {
return new RemoteCollectionItems[size];
}
};
/** Builder class for {@link RemoteCollectionItems} objects.*/
public static final class Builder {
private final LongArray mIds = new LongArray();
private final List mViews = new ArrayList<>();
private boolean mHasStableIds;
private int mViewTypeCount;
/**
* Adds a {@link RemoteViews} to the collection.
*
* @param id Id to associate with the row. Use {@link #setHasStableIds(boolean)} to
* indicate that ids are stable across changes to the collection.
* @param view RemoteViews to display for the row.
*/
@NonNull
// Covered by getItemId, getItemView, getItemCount.
@SuppressLint("MissingGetterMatchingBuilder")
public Builder addItem(long id, @NonNull RemoteViews view) {
if (view == null) throw new NullPointerException();
if (view.hasMultipleLayouts()) {
throw new IllegalArgumentException(
"RemoteViews used in a RemoteCollectionItems cannot specify separate "
+ "layouts for orientations or sizes.");
}
mIds.add(id);
mViews.add(view);
return this;
}
/**
* Sets whether the item ids are stable across changes to the underlying data.
*
* @see android.widget.Adapter#hasStableIds()
*/
@NonNull
public Builder setHasStableIds(boolean hasStableIds) {
mHasStableIds = hasStableIds;
return this;
}
/**
* Sets the view type count for the collection when used in an adapter. This can be set
* to the maximum number of different layout ids that will be used by RemoteViews in
* this collection.
*
* If this value is not set, then a value will be inferred from the provided items. As
* a result, the adapter may need to be recreated when the list is updated with
* previously unseen RemoteViews layouts for new items.
*
* @see android.widget.Adapter#getViewTypeCount()
*/
@NonNull
public Builder setViewTypeCount(int viewTypeCount) {
mViewTypeCount = viewTypeCount;
return this;
}
/** Creates the {@link RemoteCollectionItems} defined by this builder. */
@NonNull
public RemoteCollectionItems build() {
if (mViewTypeCount < 1) {
// If a view type count wasn't specified, set it to be the number of distinct
// layout ids used in the items.
mViewTypeCount = (int) mViews.stream()
.mapToInt(RemoteViews::getLayoutId)
.distinct()
.count();
}
return new RemoteCollectionItems(
mIds.toArray(),
mViews.toArray(new RemoteViews[0]),
mHasStableIds,
Math.max(mViewTypeCount, 1));
}
}
}
/**
* Get the ID of the top-level view of the XML layout, if set using
* {@link RemoteViews#RemoteViews(String, int, int)}.
*/
public @IdRes 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;
}
}