/*
|
* 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.ColorInt;
|
import android.annotation.DimenRes;
|
import android.annotation.IntDef;
|
import android.annotation.LayoutRes;
|
import android.annotation.NonNull;
|
import android.annotation.StyleRes;
|
import android.annotation.UnsupportedAppUsage;
|
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.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.graphics.Bitmap;
|
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.Parcelable;
|
import android.os.Process;
|
import android.os.StrictMode;
|
import android.os.UserHandle;
|
import android.text.TextUtils;
|
import android.util.ArrayMap;
|
import android.util.IntArray;
|
import android.util.Log;
|
import android.util.Pair;
|
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.ViewStub;
|
import android.widget.AdapterView.OnItemClickListener;
|
|
import com.android.internal.R;
|
import com.android.internal.util.ContrastColorUtil;
|
import com.android.internal.util.Preconditions;
|
|
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.ArrayList;
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Stack;
|
import java.util.concurrent.Executor;
|
import java.util.function.Consumer;
|
|
/**
|
* A class that describes a view hierarchy that can be displayed in
|
* another process. The hierarchy is inflated from a layout resource
|
* file, and this class provides some basic operations for modifying
|
* the content of the inflated hierarchy.
|
*
|
* <p>{@code RemoteViews} is limited to support for the following layouts:</p>
|
* <ul>
|
* <li>{@link android.widget.AdapterViewFlipper}</li>
|
* <li>{@link android.widget.FrameLayout}</li>
|
* <li>{@link android.widget.GridLayout}</li>
|
* <li>{@link android.widget.GridView}</li>
|
* <li>{@link android.widget.LinearLayout}</li>
|
* <li>{@link android.widget.ListView}</li>
|
* <li>{@link android.widget.RelativeLayout}</li>
|
* <li>{@link android.widget.StackView}</li>
|
* <li>{@link android.widget.ViewFlipper}</li>
|
* </ul>
|
* <p>And the following widgets:</p>
|
* <ul>
|
* <li>{@link android.widget.AnalogClock}</li>
|
* <li>{@link android.widget.Button}</li>
|
* <li>{@link android.widget.Chronometer}</li>
|
* <li>{@link android.widget.ImageButton}</li>
|
* <li>{@link android.widget.ImageView}</li>
|
* <li>{@link android.widget.ProgressBar}</li>
|
* <li>{@link android.widget.TextClock}</li>
|
* <li>{@link android.widget.TextView}</li>
|
* </ul>
|
* <p>Descendants of these classes are not supported.</p>
|
*/
|
public class RemoteViews implements Parcelable, Filter {
|
|
private static final String LOG_TAG = "RemoteViews";
|
|
/**
|
* 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;
|
|
// 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;
|
|
/** @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;
|
|
/**
|
* Application that hosts the remote views.
|
*
|
* @hide
|
*/
|
@UnsupportedAppUsage
|
public ApplicationInfo mApplication;
|
|
/**
|
* The resource ID of the layout file. (Added to the parcel)
|
*/
|
@UnsupportedAppUsage
|
private final int mLayoutId;
|
|
/**
|
* The resource ID of the layout file in dark text mode. (Added to the parcel)
|
*/
|
private int mLightBackgroundLayoutId = 0;
|
|
/**
|
* An array of actions to perform on the view tree once it has been
|
* inflated
|
*/
|
@UnsupportedAppUsage
|
private ArrayList<Action> mActions;
|
|
/**
|
* Maps bitmaps to unique indicies to avoid Bitmap duplication.
|
*/
|
@UnsupportedAppUsage
|
private BitmapCache mBitmapCache;
|
|
/**
|
* 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;
|
|
/**
|
* 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
|
private RemoteViews mPortrait = null;
|
|
@ApplyFlags
|
private int mApplyFlags = 0;
|
|
/** Class cookies of the Parcel this instance was read from. */
|
private final Map<Class, Object> mClassCookies;
|
|
private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = (view, pendingIntent, response)
|
-> startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
|
|
private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>();
|
|
/**
|
* This key is used to perform lookups in sMethods without causing allocations.
|
*/
|
private static final MethodKey sLookupKey = new MethodKey();
|
|
/**
|
* @hide
|
*/
|
public void setRemoteInputs(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<Bitmap> cache = mBitmapCache.mBitmaps;
|
for (int i = 0; i < cache.size(); i++) {
|
Bitmap bitmap = cache.get(i);
|
cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
|
}
|
}
|
|
/**
|
* 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(int viewId, 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(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);
|
}
|
}
|
|
/** @hide */
|
public interface OnClickHandler {
|
|
/** @hide */
|
boolean onClickHandler(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,
|
OnClickHandler handler) 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
|
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, OnClickHandler handler) {
|
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<Uri> visitor) {
|
// Nothing to visit by default
|
}
|
|
@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, OnClickHandler handler) { }
|
};
|
|
/**
|
* Merges the passed RemoteViews actions with this RemoteViews actions according to
|
* action-specific merge rules.
|
*
|
* @param newRv
|
*
|
* @hide
|
*/
|
@UnsupportedAppUsage
|
public void mergeRemoteViews(RemoteViews newRv) {
|
if (newRv == null) return;
|
// We first copy the new RemoteViews, as the process of merging modifies the way the actions
|
// reference the bitmap cache. We don't want to modify the object as it may need to
|
// be merged and applied multiple times.
|
RemoteViews copy = new RemoteViews(newRv);
|
|
HashMap<String, Action> map = new HashMap<String, Action>();
|
if (mActions == null) {
|
mActions = new ArrayList<Action>();
|
}
|
|
int count = mActions.size();
|
for (int i = 0; i < count; i++) {
|
Action a = mActions.get(i);
|
map.put(a.getUniqueKey(), a);
|
}
|
|
ArrayList<Action> newActions = copy.mActions;
|
if (newActions == null) return;
|
count = newActions.size();
|
for (int i = 0; i < count; i++) {
|
Action a = newActions.get(i);
|
String key = newActions.get(i).getUniqueKey();
|
int mergeBehavior = newActions.get(i).mergeBehavior();
|
if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) {
|
mActions.remove(map.get(key));
|
map.remove(key);
|
}
|
|
// If the merge behavior is ignore, we don't bother keeping the extra action
|
if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) {
|
mActions.add(a);
|
}
|
}
|
|
// Because pruning can remove the need for bitmaps, we reconstruct the 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<Uri> 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<Uri> visitor) {
|
if (icon != null && icon.getType() == Icon.TYPE_URI) {
|
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();
|
}
|
}
|
|
private class SetEmptyView extends Action {
|
int emptyViewId;
|
|
SetEmptyView(int viewId, 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, OnClickHandler handler) {
|
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(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 OnClickHandler handler) {
|
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 = new OnItemClickListener() {
|
public void onItemClick(AdapterView<?> parent, View view,
|
int position, long id) {
|
// The view should be a frame layout
|
if (view instanceof ViewGroup) {
|
ViewGroup vg = (ViewGroup) view;
|
|
// AdapterViews contain their children in a frame
|
// so we need to go one layer deeper here.
|
if (parent instanceof AdapterViewAnimator) {
|
vg = (ViewGroup) vg.getChildAt(0);
|
}
|
if (vg == null) return;
|
|
RemoteResponse response = null;
|
int childCount = vg.getChildCount();
|
for (int i = 0; i < childCount; i++) {
|
Object tag = vg.getChildAt(i).getTag(com.android.internal.R.id.fillInIntent);
|
if (tag instanceof RemoteResponse) {
|
response = (RemoteResponse) tag;
|
break;
|
}
|
}
|
if (response == null) return;
|
response.handleViewClick(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;
|
}
|
}
|
|
@Override
|
public int getActionTag() {
|
return SET_PENDING_INTENT_TEMPLATE_TAG;
|
}
|
|
@UnsupportedAppUsage
|
PendingIntent pendingIntentTemplate;
|
}
|
|
private class SetRemoteViewsAdapterList extends Action {
|
public SetRemoteViewsAdapterList(int id, ArrayList<RemoteViews> 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, OnClickHandler handler) {
|
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));
|
}
|
} 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));
|
}
|
}
|
}
|
|
@Override
|
public int getActionTag() {
|
return SET_REMOTE_VIEW_ADAPTER_LIST_TAG;
|
}
|
|
int viewTypeCount;
|
ArrayList<RemoteViews> list;
|
}
|
|
private class SetRemoteViewsAdapterIntent extends Action {
|
public SetRemoteViewsAdapterIntent(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, OnClickHandler handler) {
|
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;
|
}
|
|
// 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.setRemoteViewsOnClickHandler(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,
|
OnClickHandler handler) {
|
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(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 OnClickHandler handler) {
|
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
|
target.setOnClickListener(null);
|
return;
|
}
|
target.setOnClickListener(v -> mResponse.handleViewClick(v, handler));
|
}
|
|
@Override
|
public int getActionTag() {
|
return SET_ON_CLICK_RESPONSE_TAG;
|
}
|
|
final RemoteResponse mResponse;
|
}
|
|
/** @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;
|
}
|
|
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.
|
* <p>
|
* The operation will be performed on the {@link Drawable} returned by the
|
* target {@link View#getBackground()} by default. If targetBackground is false,
|
* we assume the target is an {@link ImageView} and try applying the operations
|
* to {@link ImageView#getDrawable()}.
|
* <p>
|
*/
|
private class SetDrawableTint extends Action {
|
SetDrawableTint(int id, boolean targetBackground,
|
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, OnClickHandler handler) {
|
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;
|
int colorFilter;
|
PorterDuff.Mode filterMode;
|
}
|
|
/**
|
* Equivalent to calling
|
* {@link RippleDrawable#setColor(ColorStateList)},
|
* on the {@link Drawable} of a given view.
|
* <p>
|
* The operation will be performed on the {@link Drawable} returned by the
|
* target {@link View#getBackground()}.
|
* <p>
|
*/
|
private class SetRippleDrawableColor extends Action {
|
|
ColorStateList mColorStateList;
|
|
SetRippleDrawableColor(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, OnClickHandler handler) {
|
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(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, OnClickHandler handler) {
|
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
|
ArrayList<Bitmap> 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);
|
}
|
}
|
}
|
|
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(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.readString();
|
bitmapId = in.readInt();
|
bitmap = mBitmapCache.getBitmapForId(bitmapId);
|
}
|
|
@Override
|
public void writeToParcel(Parcel dest, int flags) {
|
dest.writeInt(viewId);
|
dest.writeString(methodName);
|
dest.writeInt(bitmapId);
|
}
|
|
@Override
|
public void apply(View root, ViewGroup rootParent,
|
OnClickHandler handler) throws ActionException {
|
ReflectionAction ra = new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP,
|
bitmap);
|
ra.apply(root, rootParent, handler);
|
}
|
|
@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 final class ReflectionAction 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;
|
|
@UnsupportedAppUsage
|
String methodName;
|
int type;
|
@UnsupportedAppUsage
|
Object value;
|
|
ReflectionAction(int viewId, String methodName, int type, Object value) {
|
this.viewId = viewId;
|
this.methodName = methodName;
|
this.type = type;
|
this.value = value;
|
}
|
|
ReflectionAction(Parcel in) {
|
this.viewId = in.readInt();
|
this.methodName = in.readString();
|
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);
|
}
|
|
// 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.readString();
|
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);
|
default:
|
break;
|
}
|
}
|
|
public void writeToParcel(Parcel out, int flags) {
|
out.writeInt(this.viewId);
|
out.writeString(this.methodName);
|
out.writeInt(this.type);
|
//noinspection ConstantIfStatement
|
if (false) {
|
Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
|
+ " methodName=" + this.methodName + " type=" + this.type);
|
}
|
|
// 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.writeString((String)this.value);
|
break;
|
case CHAR_SEQUENCE:
|
TextUtils.writeToParcel((CharSequence)this.value, out, flags);
|
break;
|
case BUNDLE:
|
out.writeBundle((Bundle) this.value);
|
break;
|
case URI:
|
case BITMAP:
|
case INTENT:
|
case COLOR_STATE_LIST:
|
case ICON:
|
out.writeTypedObject((Parcelable) this.value, flags);
|
break;
|
default:
|
break;
|
}
|
}
|
|
private Class<?> getParameterType() {
|
switch (this.type) {
|
case BOOLEAN:
|
return boolean.class;
|
case BYTE:
|
return byte.class;
|
case SHORT:
|
return short.class;
|
case INT:
|
return int.class;
|
case LONG:
|
return long.class;
|
case FLOAT:
|
return float.class;
|
case DOUBLE:
|
return double.class;
|
case CHAR:
|
return char.class;
|
case STRING:
|
return String.class;
|
case CHAR_SEQUENCE:
|
return CharSequence.class;
|
case URI:
|
return Uri.class;
|
case BITMAP:
|
return Bitmap.class;
|
case BUNDLE:
|
return Bundle.class;
|
case INTENT:
|
return Intent.class;
|
case COLOR_STATE_LIST:
|
return ColorStateList.class;
|
case ICON:
|
return Icon.class;
|
default:
|
return null;
|
}
|
}
|
|
@Override
|
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
|
final View view = root.findViewById(viewId);
|
if (view == null) return;
|
|
Class<?> param = getParameterType();
|
if (param == null) {
|
throw new ActionException("bad type: " + this.type);
|
}
|
try {
|
getMethod(view, this.methodName, param, false /* async */).invoke(view, this.value);
|
} catch (Throwable ex) {
|
throw new ActionException(ex);
|
}
|
}
|
|
@Override
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
|
final View view = root.findViewById(viewId);
|
if (view == null) return ACTION_NOOP;
|
|
Class<?> param = getParameterType();
|
if (param == null) {
|
throw new ActionException("bad type: " + this.type);
|
}
|
|
try {
|
MethodHandle method = getMethod(view, this.methodName, param, true /* async */);
|
|
if (method != null) {
|
Runnable endAction = (Runnable) method.invoke(view, this.value);
|
if (endAction == null) {
|
return ACTION_NOOP;
|
} else {
|
// 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 int mergeBehavior() {
|
// smoothScrollBy is cumulative, everything else overwites.
|
if (methodName.equals("smoothScrollBy")) {
|
return MERGE_APPEND;
|
} else {
|
return MERGE_REPLACE;
|
}
|
}
|
|
@Override
|
public int getActionTag() {
|
return REFLECTION_ACTION_TAG;
|
}
|
|
@Override
|
public 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 boolean prefersAsyncApply() {
|
return this.type == URI || this.type == ICON;
|
}
|
|
@Override
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
switch (this.type) {
|
case URI:
|
final Uri uri = (Uri) this.value;
|
visitor.accept(uri);
|
break;
|
case ICON:
|
final Icon icon = (Icon) this.value;
|
visitIconUri(icon, visitor);
|
break;
|
}
|
}
|
}
|
|
/**
|
* 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, OnClickHandler handler) {
|
mRunnable.run();
|
}
|
}
|
|
private void configureRemoteViewsAsChild(RemoteViews rv) {
|
rv.setBitmapCache(mBitmapCache);
|
rv.setNotRoot();
|
}
|
|
void setNotRoot() {
|
mIsRoot = false;
|
}
|
|
/**
|
* ViewGroup methods that are related to adding Views.
|
*/
|
private class ViewGroupActionAdd extends Action {
|
@UnsupportedAppUsage
|
private RemoteViews mNestedViews;
|
private int mIndex;
|
|
ViewGroupActionAdd(int viewId, RemoteViews nestedViews) {
|
this(viewId, nestedViews, -1 /* index */);
|
}
|
|
ViewGroupActionAdd(int viewId, RemoteViews nestedViews, int index) {
|
this.viewId = viewId;
|
mNestedViews = nestedViews;
|
mIndex = index;
|
if (nestedViews != null) {
|
configureRemoteViewsAsChild(nestedViews);
|
}
|
}
|
|
ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info,
|
int depth, Map<Class, Object> classCookies) {
|
viewId = parcel.readInt();
|
mIndex = 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);
|
mNestedViews.writeToParcel(dest, flags);
|
}
|
|
@Override
|
public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
|
return mNestedViews.hasSameAppInfo(parentInfo);
|
}
|
|
@Override
|
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
|
final Context context = root.getContext();
|
final ViewGroup target = root.findViewById(viewId);
|
|
if (target == null) {
|
return;
|
}
|
|
// Inflate nested views and add as children
|
target.addView(mNestedViews.apply(context, target, handler), mIndex);
|
}
|
|
@Override
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
|
// 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();
|
final AsyncApplyTask task = mNestedViews.getAsyncApplyTask(
|
context, targetVg, null, handler);
|
final ViewTree tree = task.doInBackground();
|
|
if (tree == null) {
|
throw new ActionException(task.mError);
|
}
|
|
// Update the global view tree, so that next call to findViewTreeById
|
// goes through the subtree as well.
|
target.addChild(tree, mIndex);
|
|
return new RuntimeAction() {
|
@Override
|
public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
|
throws ActionException {
|
task.onPostExecute(tree);
|
targetVg.addView(task.mResult, mIndex);
|
}
|
};
|
}
|
|
@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.
|
*
|
* <p>Using -2 because the default id is -1. This avoids accidentally matching that.
|
*/
|
private static final int REMOVE_ALL_VIEWS_ID = -2;
|
|
private int mViewIdToKeep;
|
|
ViewGroupActionRemove(int viewId) {
|
this(viewId, REMOVE_ALL_VIEWS_ID);
|
}
|
|
ViewGroupActionRemove(int viewId, 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, OnClickHandler handler) {
|
final ViewGroup target = root.findViewById(viewId);
|
|
if (target == null) {
|
return;
|
}
|
|
if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
|
target.removeAllViews();
|
return;
|
}
|
|
removeAllViewsExceptIdToKeep(target);
|
}
|
|
@Override
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
|
// 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;
|
|
// Clear all children when nested views omitted
|
target.mChildren = null;
|
return new RuntimeAction() {
|
@Override
|
public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
|
throws ActionException {
|
if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
|
targetVg.removeAllViews();
|
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;
|
}
|
}
|
|
/**
|
* 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(int viewId, boolean isRelative, int d1, int d2, int d3, 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(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, OnClickHandler handler) {
|
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, OnClickHandler handler) {
|
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<Uri> 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 {
|
public TextViewSizeAction(int viewId, int units, float size) {
|
this.viewId = viewId;
|
this.units = units;
|
this.size = size;
|
}
|
|
public 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, OnClickHandler handler) {
|
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(int viewId, int left, int top, int right, 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, OnClickHandler handler) {
|
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;
|
}
|
|
int left, top, right, bottom;
|
}
|
|
/**
|
* Helper action to set layout params on a View.
|
*/
|
private static class LayoutParamAction extends Action {
|
|
/** Set marginEnd */
|
public static final int LAYOUT_MARGIN_END_DIMEN = 1;
|
/** Set width */
|
public static final int LAYOUT_WIDTH = 2;
|
public static final int LAYOUT_MARGIN_BOTTOM_DIMEN = 3;
|
public static final int LAYOUT_MARGIN_END = 4;
|
|
final int mProperty;
|
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
|
*/
|
public LayoutParamAction(int viewId, int property, int value) {
|
this.viewId = viewId;
|
this.mProperty = property;
|
this.mValue = value;
|
}
|
|
public LayoutParamAction(Parcel parcel) {
|
viewId = parcel.readInt();
|
mProperty = parcel.readInt();
|
mValue = parcel.readInt();
|
}
|
|
public void writeToParcel(Parcel dest, int flags) {
|
dest.writeInt(viewId);
|
dest.writeInt(mProperty);
|
dest.writeInt(mValue);
|
}
|
|
@Override
|
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
|
final View target = root.findViewById(viewId);
|
if (target == null) {
|
return;
|
}
|
ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
|
if (layoutParams == null) {
|
return;
|
}
|
int value = mValue;
|
switch (mProperty) {
|
case LAYOUT_MARGIN_END_DIMEN:
|
value = resolveDimenPixelOffset(target, mValue);
|
// fall-through
|
case LAYOUT_MARGIN_END:
|
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(value);
|
target.setLayoutParams(layoutParams);
|
}
|
break;
|
case LAYOUT_MARGIN_BOTTOM_DIMEN:
|
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
int resolved = resolveDimenPixelOffset(target, mValue);
|
((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = resolved;
|
target.setLayoutParams(layoutParams);
|
}
|
break;
|
case LAYOUT_WIDTH:
|
layoutParams.width = mValue;
|
target.setLayoutParams(layoutParams);
|
break;
|
default:
|
throw new IllegalArgumentException("Unknown property " + mProperty);
|
}
|
}
|
|
private static int resolveDimenPixelOffset(View target, int value) {
|
if (value == 0) {
|
return 0;
|
}
|
return target.getContext().getResources().getDimensionPixelOffset(value);
|
}
|
|
@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(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, OnClickHandler handler) {
|
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, OnClickHandler handler) {
|
// Let's traverse the viewtree and override all textColors!
|
Stack<View> 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 {
|
private final int mViewId;
|
private final int mKey;
|
private final int mTag;
|
|
SetIntTagAction(int viewId, 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, OnClickHandler handler) {
|
final View target = root.findViewById(mViewId);
|
if (target == null) return;
|
|
target.setTagInternal(mKey, mTag);
|
}
|
|
@Override
|
public int getActionTag() {
|
return SET_INT_TAG_TAG;
|
}
|
}
|
|
/**
|
* Create a new RemoteViews object that will display the views contained
|
* in the specified layout file.
|
*
|
* @param packageName Name of the package that contains the layout resource
|
* @param layoutId The id of the layout resource
|
*/
|
public RemoteViews(String packageName, int layoutId) {
|
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
|
}
|
|
/**
|
* Create a new RemoteViews object that will display the views contained
|
* in the specified layout file.
|
*
|
* @param packageName Name of the package that contains the layout resource.
|
* @param userId The user under which the package is running.
|
* @param layoutId The id of the layout resource.
|
*
|
* @hide
|
*/
|
public RemoteViews(String packageName, int userId, @LayoutRes int layoutId) {
|
this(getApplicationInfo(packageName, userId), layoutId);
|
}
|
|
/**
|
* 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 hasLandscapeAndPortraitLayouts() {
|
return (mLandscape != null) && (mPortrait != null);
|
}
|
|
/**
|
* 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
|
*/
|
public RemoteViews(RemoteViews landscape, RemoteViews portrait) {
|
if (landscape == null || portrait == null) {
|
throw new RuntimeException("Both RemoteViews must be non-null");
|
}
|
if (!landscape.hasSameAppInfo(portrait.mApplication)) {
|
throw new RuntimeException("Both RemoteViews must share the same package and user");
|
}
|
mApplication = portrait.mApplication;
|
mLayoutId = portrait.mLayoutId;
|
mLightBackgroundLayoutId = portrait.mLightBackgroundLayoutId;
|
|
mLandscape = landscape;
|
mPortrait = portrait;
|
|
mBitmapCache = new BitmapCache();
|
configureRemoteViewsAsChild(landscape);
|
configureRemoteViewsAsChild(portrait);
|
|
mClassCookies = (portrait.mClassCookies != null)
|
? portrait.mClassCookies : landscape.mClassCookies;
|
}
|
|
/**
|
* Creates a copy of another RemoteViews.
|
*/
|
public RemoteViews(RemoteViews src) {
|
mBitmapCache = src.mBitmapCache;
|
mApplication = src.mApplication;
|
mIsRoot = src.mIsRoot;
|
mLayoutId = src.mLayoutId;
|
mLightBackgroundLayoutId = src.mLightBackgroundLayoutId;
|
mApplyFlags = src.mApplyFlags;
|
mClassCookies = src.mClassCookies;
|
|
if (src.hasLandscapeAndPortraitLayouts()) {
|
mLandscape = new RemoteViews(src.mLandscape);
|
mPortrait = new RemoteViews(src.mPortrait);
|
}
|
|
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<Class, Object> 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);
|
mLayoutId = parcel.readInt();
|
mLightBackgroundLayoutId = parcel.readInt();
|
|
readActionsFromParcel(parcel, depth);
|
} 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;
|
mLightBackgroundLayoutId = mPortrait.mLightBackgroundLayoutId;
|
}
|
mApplyFlags = parcel.readInt();
|
}
|
|
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);
|
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 (!hasLandscapeAndPortraitLayouts()) {
|
if (mActions != null) {
|
final int count = mActions.size();
|
for (int i= 0; i < count; ++i) {
|
mActions.get(i).setBitmapCache(bitmapCache);
|
}
|
}
|
} else {
|
mLandscape.setBitmapCache(bitmapCache);
|
mPortrait.setBitmapCache(bitmapCache);
|
}
|
}
|
|
/**
|
* Returns an estimate of the bitmap heap memory usage for this RemoteViews.
|
*/
|
/** @hide */
|
@UnsupportedAppUsage
|
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 (hasLandscapeAndPortraitLayouts()) {
|
throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
|
" layouts cannot be modified. Instead, fully configure the landscape and" +
|
" portrait layouts individually before constructing the combined layout.");
|
}
|
if (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(int viewId, RemoteViews nestedView) {
|
addAction(nestedView == null
|
? new ViewGroupActionRemove(viewId)
|
: new ViewGroupActionAdd(viewId, nestedView));
|
}
|
|
/**
|
* 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(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(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(int viewId, int viewIdToKeep) {
|
addAction(new ViewGroupActionRemove(viewId, viewIdToKeep));
|
}
|
|
/**
|
* Equivalent to calling {@link AdapterViewAnimator#showNext()}
|
*
|
* @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()}
|
*/
|
public void showNext(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(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(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(int viewId, 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(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(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(int viewId, int left, int top, int right, 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(int viewId, int start, int top, int end, 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(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(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(int viewId, 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(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(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(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(int viewId, 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(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(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(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(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(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 permitted. 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(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 permitted. 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(int viewId, Intent fillInIntent) {
|
setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent));
|
}
|
|
/**
|
* @hide
|
* Equivalent to calling
|
* {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
|
* on the {@link Drawable} of a given view.
|
* <p>
|
*
|
* @param viewId The id of the view that contains the target
|
* {@link Drawable}
|
* @param targetBackground If true, apply these parameters to the
|
* {@link Drawable} returned by
|
* {@link android.view.View#getBackground()}. Otherwise, assume
|
* the target view is an {@link ImageView} and apply them to
|
* {@link ImageView#getDrawable()}.
|
* @param colorFilter Specify a color for a
|
* {@link android.graphics.ColorFilter} for this drawable. This will be ignored if
|
* {@code mode} is {@code null}.
|
* @param mode Specify a PorterDuff mode for this drawable, or null to leave
|
* unchanged.
|
*/
|
public void setDrawableTint(int viewId, boolean targetBackground,
|
int colorFilter, @NonNull PorterDuff.Mode mode) {
|
addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode));
|
}
|
|
/**
|
* @hide
|
* Equivalent to calling
|
* {@link RippleDrawable#setColor(ColorStateList)} on the {@link Drawable} of a given view,
|
* assuming it's a {@link RippleDrawable}.
|
* <p>
|
*
|
* @param viewId The id of the view that contains the target
|
* {@link RippleDrawable}
|
* @param colorStateList Specify a color for a
|
* {@link ColorStateList} for this drawable.
|
*/
|
public void setRippleDrawableColor(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(int viewId, ColorStateList tint) {
|
addAction(new ReflectionAction(viewId, "setProgressTintList",
|
ReflectionAction.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(int viewId, ColorStateList tint) {
|
addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList",
|
ReflectionAction.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(int viewId, ColorStateList tint) {
|
addAction(new ReflectionAction(viewId, "setIndeterminateTintList",
|
ReflectionAction.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(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(int viewId, @ColorInt ColorStateList colors) {
|
addAction(new ReflectionAction(viewId, "setTextColor", ReflectionAction.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, 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(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
|
@Deprecated
|
public void setRemoteAdapter(int viewId, ArrayList<RemoteViews> list, int viewTypeCount) {
|
addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount));
|
}
|
|
/**
|
* 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(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(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(int viewId, int left, int top, int right, int bottom) {
|
addAction(new ViewPaddingAction(viewId, left, top, right, bottom));
|
}
|
|
/**
|
* @hide
|
* Equivalent to calling {@link android.view.ViewGroup.MarginLayoutParams#setMarginEnd(int)}.
|
* Only works if the {@link View#getLayoutParams()} supports margins.
|
* Hidden for now since we don't want to support this for all different layout margins yet.
|
*
|
* @param viewId The id of the view to change
|
* @param endMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
|
*/
|
public void setViewLayoutMarginEndDimen(int viewId, @DimenRes int endMarginDimen) {
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_END_DIMEN,
|
endMarginDimen));
|
}
|
|
/**
|
* Equivalent to calling {@link android.view.ViewGroup.MarginLayoutParams#setMarginEnd(int)}.
|
* Only works if the {@link View#getLayoutParams()} supports margins.
|
* Hidden for now since we don't want to support this for all different layout margins yet.
|
*
|
* @param viewId The id of the view to change
|
* @param endMargin a value in pixels for the end margin.
|
* @hide
|
*/
|
public void setViewLayoutMarginEnd(int viewId, @DimenRes int endMargin) {
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_END,
|
endMargin));
|
}
|
|
/**
|
* Equivalent to setting {@link android.view.ViewGroup.MarginLayoutParams#bottomMargin}.
|
*
|
* @param bottomMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
|
* @hide
|
*/
|
public void setViewLayoutMarginBottomDimen(int viewId, @DimenRes int bottomMarginDimen) {
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_BOTTOM_DIMEN,
|
bottomMarginDimen));
|
}
|
|
/**
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width}.
|
*
|
* @param layoutWidth one of 0, MATCH_PARENT or WRAP_CONTENT. Other sizes are not allowed
|
* because they behave poorly when the density changes.
|
* @hide
|
*/
|
public void setViewLayoutWidth(int viewId, int layoutWidth) {
|
if (layoutWidth != 0 && layoutWidth != ViewGroup.LayoutParams.MATCH_PARENT
|
&& layoutWidth != ViewGroup.LayoutParams.WRAP_CONTENT) {
|
throw new IllegalArgumentException("Only supports 0, WRAP_CONTENT and MATCH_PARENT");
|
}
|
mActions.add(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, layoutWidth));
|
}
|
|
/**
|
* 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(int viewId, String methodName, boolean value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, byte value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, short value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, int value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, 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 value The value to pass to the method.
|
*
|
* @hide
|
*/
|
public void setColorStateList(int viewId, String methodName, ColorStateList value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.COLOR_STATE_LIST,
|
value));
|
}
|
|
|
/**
|
* 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(int viewId, String methodName, long value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, float value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value));
|
}
|
|
/**
|
* 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(int viewId, String methodName, double value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, char value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, String value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, CharSequence value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
|
}
|
|
/**
|
* 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(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, ReflectionAction.URI, value));
|
}
|
|
/**
|
* Call a method taking one Bitmap on a view in the layout for this RemoteViews.
|
* @more
|
* <p class="note">The bitmap will be flattened into the parcel if this object is
|
* sent across processes, so it may end up using a lot of memory, and may be fairly slow.</p>
|
*
|
* @param viewId The id of the view on which to call the method.
|
* @param methodName The name of the method to call.
|
* @param value The value to pass to the method.
|
*/
|
public void setBitmap(int viewId, String methodName, Bitmap value) {
|
addAction(new BitmapReflectionAction(viewId, methodName, 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(int viewId, String methodName, Bundle value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, Intent value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.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(int viewId, String methodName, Icon value) {
|
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.ICON, value));
|
}
|
|
/**
|
* 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(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(int viewId, 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(int viewId, 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(int viewId, int labeledId) {
|
setInt(viewId, "setLabelFor", labeledId);
|
}
|
|
/**
|
* 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;
|
} else {
|
return mPortrait;
|
}
|
}
|
return this;
|
}
|
|
/**
|
* Inflates the view hierarchy represented by this object and applies
|
* all of the actions.
|
*
|
* <p><strong>Caller beware: this may throw</strong>
|
*
|
* @param context Default context to use
|
* @param parent Parent that the resulting view hierarchy will be attached to. This method
|
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
|
* @return The inflated view hierarchy
|
*/
|
public View apply(Context context, ViewGroup parent) {
|
return apply(context, parent, null);
|
}
|
|
/** @hide */
|
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
|
RemoteViews rvToApply = getRemoteViewsToApply(context);
|
|
View result = inflateView(context, rvToApply, parent);
|
rvToApply.performApply(result, parent, handler);
|
return result;
|
}
|
|
/** @hide */
|
public View applyWithTheme(Context context, ViewGroup parent, OnClickHandler handler,
|
@StyleRes int applyThemeResId) {
|
RemoteViews rvToApply = getRemoteViewsToApply(context);
|
|
View result = inflateView(context, rvToApply, parent, applyThemeResId);
|
rvToApply.performApply(result, parent, handler);
|
return result;
|
}
|
|
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
|
return inflateView(context, rv, parent, 0);
|
}
|
|
private View inflateView(Context context, RemoteViews rv, ViewGroup parent,
|
@StyleRes int applyThemeResId) {
|
// 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);
|
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)
|
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
// 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(this);
|
View v = inflater.inflate(rv.getLayoutId(), parent, false);
|
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
|
return v;
|
}
|
|
/**
|
* Implement this interface to receive a callback when
|
* {@link #applyAsync} or {@link #reapplyAsync} is finished.
|
* @hide
|
*/
|
public interface OnViewAppliedListener {
|
/**
|
* Callback when the RemoteView has finished inflating,
|
* but no actions have been applied yet.
|
*/
|
default void onViewInflated(View v) {};
|
|
void onViewApplied(View v);
|
|
void onError(Exception e);
|
}
|
|
/**
|
* Applies the views asynchronously, moving as much of the task on the background
|
* thread as possible.
|
*
|
* @see #apply(Context, ViewGroup)
|
* @param context Default context to use
|
* @param parent Parent that the resulting view hierarchy will be attached to. This method
|
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
|
* @param listener the callback to run when all actions have been applied. May be null.
|
* @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used.
|
* @return CancellationSignal
|
* @hide
|
*/
|
public CancellationSignal applyAsync(
|
Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) {
|
return applyAsync(context, parent, executor, listener, null);
|
}
|
|
private CancellationSignal startTaskOnExecutor(AsyncApplyTask task, Executor executor) {
|
CancellationSignal cancelSignal = new CancellationSignal();
|
cancelSignal.setOnCancelListener(task);
|
|
task.executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor);
|
return cancelSignal;
|
}
|
|
/** @hide */
|
public CancellationSignal applyAsync(Context context, ViewGroup parent,
|
Executor executor, OnViewAppliedListener listener, OnClickHandler handler) {
|
return startTaskOnExecutor(getAsyncApplyTask(context, parent, listener, handler), executor);
|
}
|
|
private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent,
|
OnViewAppliedListener listener, OnClickHandler handler) {
|
return new AsyncApplyTask(getRemoteViewsToApply(context), parent, context, listener,
|
handler, null);
|
}
|
|
private class AsyncApplyTask extends AsyncTask<Void, Void, ViewTree>
|
implements CancellationSignal.OnCancelListener {
|
final RemoteViews mRV;
|
final ViewGroup mParent;
|
final Context mContext;
|
final OnViewAppliedListener mListener;
|
final OnClickHandler mHandler;
|
|
private View mResult;
|
private ViewTree mTree;
|
private Action[] mActions;
|
private Exception mError;
|
|
private AsyncApplyTask(
|
RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener,
|
OnClickHandler handler, View result) {
|
mRV = rv;
|
mParent = parent;
|
mContext = context;
|
mListener = listener;
|
mHandler = handler;
|
|
mResult = result;
|
}
|
|
@Override
|
protected ViewTree doInBackground(Void... params) {
|
try {
|
if (mResult == null) {
|
mResult = inflateView(mContext, mRV, mParent);
|
}
|
|
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);
|
}
|
} else {
|
mActions = null;
|
}
|
return mTree;
|
} catch (Exception e) {
|
mError = e;
|
return null;
|
}
|
}
|
|
@Override
|
protected void onPostExecute(ViewTree viewTree) {
|
if (mError == null) {
|
if (mListener != null) {
|
mListener.onViewInflated(viewTree.mRoot);
|
}
|
|
try {
|
if (mActions != null) {
|
OnClickHandler handler = mHandler == null
|
? DEFAULT_ON_CLICK_HANDLER : mHandler;
|
for (Action a : mActions) {
|
a.apply(viewTree.mRoot, mParent, handler);
|
}
|
}
|
} 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);
|
}
|
}
|
|
/**
|
* Applies all of the actions to the provided view.
|
*
|
* <p><strong>Caller beware: this may throw</strong>
|
*
|
* @param v The view to apply the actions to. This should be the result of
|
* the {@link #apply(Context,ViewGroup)} call.
|
*/
|
public void reapply(Context context, View v) {
|
reapply(context, v, null);
|
}
|
|
/** @hide */
|
public void reapply(Context context, View v, OnClickHandler handler) {
|
RemoteViews rvToApply = getRemoteViewsToApply(context);
|
|
// In the case that a view has this RemoteViews applied in one orientation, is persisted
|
// across orientation change, and has the RemoteViews re-applied in the new orientation,
|
// we throw an exception, since the layouts may be completely unrelated.
|
if (hasLandscapeAndPortraitLayouts()) {
|
if ((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.");
|
}
|
}
|
|
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
|
}
|
|
/**
|
* 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, OnClickHandler handler) {
|
RemoteViews rvToApply = getRemoteViewsToApply(context);
|
|
// In the case that a view has this RemoteViews applied in one orientation, is persisted
|
// across orientation change, and has the RemoteViews re-applied in the new orientation,
|
// we throw an exception, since the layouts may be completely unrelated.
|
if (hasLandscapeAndPortraitLayouts()) {
|
if ((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 startTaskOnExecutor(new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
|
context, listener, handler, v), executor);
|
}
|
|
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
|
if (mActions != null) {
|
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
|
final int count = mActions.size();
|
for (int i = 0; i < count; i++) {
|
Action a = mActions.get(i);
|
a.apply(v, parent, handler);
|
}
|
}
|
}
|
|
/**
|
* 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;
|
}
|
|
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;
|
}
|
|
/**
|
* 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();
|
}
|
|
/* (non-Javadoc)
|
* Used to restrict the views which can be inflated
|
*
|
* @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class)
|
*/
|
public boolean onLoadClass(Class clazz) {
|
return clazz.isAnnotationPresent(RemoteView.class);
|
}
|
|
public int describeContents() {
|
return 0;
|
}
|
|
public void writeToParcel(Parcel dest, int flags) {
|
if (!hasLandscapeAndPortraitLayouts()) {
|
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);
|
}
|
dest.writeInt(mLayoutId);
|
dest.writeInt(mLightBackgroundLayoutId);
|
writeActionsToParcel(dest);
|
} 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);
|
}
|
|
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);
|
}
|
}
|
|
private static ApplicationInfo getApplicationInfo(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<RemoteViews> CREATOR = new Parcelable.Creator<RemoteViews>() {
|
public RemoteViews createFromParcel(Parcel parcel) {
|
return new RemoteViews(parcel);
|
}
|
|
public RemoteViews[] newArray(int size) {
|
return new RemoteViews[size];
|
}
|
};
|
|
/**
|
* A representation of the view hierarchy. Only views which have a valid ID are added
|
* and can be searched.
|
*/
|
private static class ViewTree {
|
private static final int INSERT_AT_END_INDEX = -1;
|
private View mRoot;
|
private ArrayList<ViewTree> mChildren;
|
|
private ViewTree(View root) {
|
mRoot = root;
|
}
|
|
public void createTree() {
|
if (mChildren != null) {
|
return;
|
}
|
|
mChildren = new ArrayList<>();
|
if (mRoot instanceof ViewGroup) {
|
ViewGroup vg = (ViewGroup) mRoot;
|
int count = vg.getChildCount();
|
for (int i = 0; i < count; i++) {
|
addViewChild(vg.getChildAt(i));
|
}
|
}
|
}
|
|
public ViewTree findViewTreeById(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;
|
}
|
|
public void replaceView(View v) {
|
mRoot = v;
|
mChildren = null;
|
createTree();
|
}
|
|
public <T extends View> T findViewById(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);
|
}
|
|
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));
|
}
|
}
|
}
|
}
|
}
|
|
/**
|
* Class representing a response to an action performed on any element of a RemoteViews.
|
*/
|
public static class RemoteResponse {
|
|
private PendingIntent mPendingIntent;
|
private Intent mFillIntent;
|
|
private IntArray mViewIds;
|
private ArrayList<String> mElementNames;
|
|
/**
|
* Creates a response which sends a pending intent as part of the response. The source
|
* bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the
|
* target view in screen space.
|
* Note that any activity options associated with the mPendingIntent may get overridden
|
* before starting the intent.
|
*
|
* @param pendingIntent The {@link PendingIntent} to send as part of the response
|
*/
|
@NonNull
|
public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) {
|
RemoteResponse response = new RemoteResponse();
|
response.mPendingIntent = pendingIntent;
|
return response;
|
}
|
|
/**
|
* When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is
|
* very costly to set PendingIntents on the individual items, and is hence not permitted.
|
* 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)
|
* @return
|
*/
|
@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(int viewId, @NonNull String sharedElementName) {
|
if (mViewIds == null) {
|
mViewIds = new IntArray();
|
mElementNames = new ArrayList<>();
|
}
|
mViewIds.add(viewId);
|
mElementNames.add(sharedElementName);
|
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.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);
|
}
|
int[] viewIds = parcel.createIntArray();
|
mViewIds = viewIds == null ? null : IntArray.wrap(viewIds);
|
mElementNames = parcel.createStringArrayList();
|
}
|
|
private void handleViewClick(View v, OnClickHandler handler) {
|
final PendingIntent pi;
|
if (mPendingIntent != null) {
|
pi = mPendingIntent;
|
} else if (mFillIntent != null) {
|
// Insure that this view is a child of an AdapterView
|
View parent = (View) v.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();
|
}
|
|
if (!(parent instanceof AdapterView<?>)) {
|
// Somehow they've managed to get this far without having
|
// and AdapterView as a parent.
|
Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent");
|
return;
|
}
|
// Insure that a template pending intent has been set on an ancestor
|
if (!(parent.getTag() instanceof PendingIntent)) {
|
Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without"
|
+ " calling setPendingIntentTemplate on parent.");
|
return;
|
}
|
|
pi = (PendingIntent) parent.getTag();
|
} else {
|
Log.e(LOG_TAG, "Response has neither pendingIntent nor fillInIntent");
|
return;
|
}
|
|
handler.onClickHandler(v, pi, this);
|
}
|
|
/** @hide */
|
public Pair<Intent, ActivityOptions> getLaunchOptions(View view) {
|
Intent intent = mPendingIntent != null ? new Intent() : new Intent(mFillIntent);
|
intent.setSourceBounds(getSourceBounds(view));
|
|
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<Intent, ActivityOptions> options) {
|
try {
|
// TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
|
Context context = view.getContext();
|
// The NEW_TASK flags are applied through the activity options and not as a part of
|
// the call to startIntentSender() to ensure that they are consistently applied to
|
// both mutable and immutable PendingIntents.
|
context.startIntentSender(
|
pendingIntent.getIntentSender(), options.first,
|
0, 0, 0, options.second.toBundle());
|
} catch (IntentSender.SendIntentException e) {
|
Log.e(LOG_TAG, "Cannot send pending intent: ", e);
|
return false;
|
} catch (Exception e) {
|
Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e);
|
return false;
|
}
|
return true;
|
}
|
}
|