7864 lines
309 KiB
Java
7864 lines
309 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2007 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package android.widget;
|
||
|
|
||
|
import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;
|
||
|
import static android.appwidget.flags.Flags.drawDataParcel;
|
||
|
import static android.appwidget.flags.Flags.remoteAdapterConversion;
|
||
|
import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
|
||
|
|
||
|
import android.annotation.AttrRes;
|
||
|
import android.annotation.ColorInt;
|
||
|
import android.annotation.ColorRes;
|
||
|
import android.annotation.DimenRes;
|
||
|
import android.annotation.DrawableRes;
|
||
|
import android.annotation.FlaggedApi;
|
||
|
import android.annotation.IdRes;
|
||
|
import android.annotation.IntDef;
|
||
|
import android.annotation.LayoutRes;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.Px;
|
||
|
import android.annotation.StringRes;
|
||
|
import android.annotation.StyleRes;
|
||
|
import android.annotation.SuppressLint;
|
||
|
import android.app.Activity;
|
||
|
import android.app.ActivityOptions;
|
||
|
import android.app.ActivityThread;
|
||
|
import android.app.Application;
|
||
|
import android.app.LoadedApk;
|
||
|
import android.app.PendingIntent;
|
||
|
import android.app.RemoteInput;
|
||
|
import android.appwidget.AppWidgetHostView;
|
||
|
import android.compat.annotation.UnsupportedAppUsage;
|
||
|
import android.content.ComponentName;
|
||
|
import android.content.Context;
|
||
|
import android.content.ContextWrapper;
|
||
|
import android.content.Intent;
|
||
|
import android.content.IntentSender;
|
||
|
import android.content.ServiceConnection;
|
||
|
import android.content.pm.ApplicationInfo;
|
||
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||
|
import android.content.res.ColorStateList;
|
||
|
import android.content.res.Configuration;
|
||
|
import android.content.res.Resources;
|
||
|
import android.content.res.TypedArray;
|
||
|
import android.content.res.loader.ResourcesLoader;
|
||
|
import android.content.res.loader.ResourcesProvider;
|
||
|
import android.graphics.Bitmap;
|
||
|
import android.graphics.BlendMode;
|
||
|
import android.graphics.Outline;
|
||
|
import android.graphics.PorterDuff;
|
||
|
import android.graphics.Rect;
|
||
|
import android.graphics.drawable.Drawable;
|
||
|
import android.graphics.drawable.Icon;
|
||
|
import android.graphics.drawable.RippleDrawable;
|
||
|
import android.net.Uri;
|
||
|
import android.os.AsyncTask;
|
||
|
import android.os.Binder;
|
||
|
import android.os.Build;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.CancellationSignal;
|
||
|
import android.os.IBinder;
|
||
|
import android.os.Parcel;
|
||
|
import android.os.ParcelFileDescriptor;
|
||
|
import android.os.Parcelable;
|
||
|
import android.os.Process;
|
||
|
import android.os.RemoteException;
|
||
|
import android.os.StrictMode;
|
||
|
import android.os.UserHandle;
|
||
|
import android.system.Os;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.DisplayMetrics;
|
||
|
import android.util.IntArray;
|
||
|
import android.util.Log;
|
||
|
import android.util.LongArray;
|
||
|
import android.util.Pair;
|
||
|
import android.util.SizeF;
|
||
|
import android.util.SparseArray;
|
||
|
import android.util.SparseIntArray;
|
||
|
import android.util.TypedValue;
|
||
|
import android.util.TypedValue.ComplexDimensionUnit;
|
||
|
import android.view.ContextThemeWrapper;
|
||
|
import android.view.LayoutInflater;
|
||
|
import android.view.LayoutInflater.Filter;
|
||
|
import android.view.MotionEvent;
|
||
|
import android.view.RemotableViewMethod;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.view.ViewGroup.MarginLayoutParams;
|
||
|
import android.view.ViewManager;
|
||
|
import android.view.ViewOutlineProvider;
|
||
|
import android.view.ViewParent;
|
||
|
import android.view.ViewStub;
|
||
|
import android.widget.AdapterView.OnItemClickListener;
|
||
|
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||
|
|
||
|
import com.android.internal.R;
|
||
|
import com.android.internal.util.Preconditions;
|
||
|
import com.android.internal.widget.IRemoteViewsFactory;
|
||
|
import com.android.internal.widget.remotecompose.core.operations.Theme;
|
||
|
import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
|
||
|
import com.android.internal.widget.remotecompose.player.RemoteComposePlayer;
|
||
|
|
||
|
import java.io.ByteArrayInputStream;
|
||
|
import java.io.ByteArrayOutputStream;
|
||
|
import java.io.FileDescriptor;
|
||
|
import java.io.FileOutputStream;
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
|
import java.io.OutputStream;
|
||
|
import java.lang.annotation.ElementType;
|
||
|
import java.lang.annotation.Retention;
|
||
|
import java.lang.annotation.RetentionPolicy;
|
||
|
import java.lang.annotation.Target;
|
||
|
import java.lang.invoke.MethodHandle;
|
||
|
import java.lang.invoke.MethodHandles;
|
||
|
import java.lang.invoke.MethodType;
|
||
|
import java.lang.reflect.Method;
|
||
|
import java.util.ArrayDeque;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.Iterator;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.Objects;
|
||
|
import java.util.concurrent.CompletableFuture;
|
||
|
import java.util.concurrent.Executor;
|
||
|
import java.util.concurrent.TimeUnit;
|
||
|
import java.util.function.Consumer;
|
||
|
import java.util.function.Predicate;
|
||
|
|
||
|
/**
|
||
|
* A class that describes a view hierarchy that can be displayed in
|
||
|
* another process. The hierarchy is inflated from a layout resource
|
||
|
* file, and this class provides some basic operations for modifying
|
||
|
* the content of the inflated hierarchy.
|
||
|
*
|
||
|
* <p>{@code RemoteViews} is limited to support for the following layouts:</p>
|
||
|
* <ul>
|
||
|
* <li>{@link android.widget.AdapterViewFlipper}</li>
|
||
|
* <li>{@link android.widget.FrameLayout}</li>
|
||
|
* <li>{@link android.widget.GridLayout}</li>
|
||
|
* <li>{@link android.widget.GridView}</li>
|
||
|
* <li>{@link android.widget.LinearLayout}</li>
|
||
|
* <li>{@link android.widget.ListView}</li>
|
||
|
* <li>{@link android.widget.RelativeLayout}</li>
|
||
|
* <li>{@link android.widget.StackView}</li>
|
||
|
* <li>{@link android.widget.ViewFlipper}</li>
|
||
|
* </ul>
|
||
|
* <p>And the following widgets:</p>
|
||
|
* <ul>
|
||
|
* <li>{@link android.widget.AnalogClock}</li>
|
||
|
* <li>{@link android.widget.Button}</li>
|
||
|
* <li>{@link android.widget.Chronometer}</li>
|
||
|
* <li>{@link android.widget.ImageButton}</li>
|
||
|
* <li>{@link android.widget.ImageView}</li>
|
||
|
* <li>{@link android.widget.ProgressBar}</li>
|
||
|
* <li>{@link android.widget.TextClock}</li>
|
||
|
* <li>{@link android.widget.TextView}</li>
|
||
|
* </ul>
|
||
|
* <p>As of API 31, the following widgets and layouts may also be used:</p>
|
||
|
* <ul>
|
||
|
* <li>{@link android.widget.CheckBox}</li>
|
||
|
* <li>{@link android.widget.RadioButton}</li>
|
||
|
* <li>{@link android.widget.RadioGroup}</li>
|
||
|
* <li>{@link android.widget.Switch}</li>
|
||
|
* </ul>
|
||
|
* <p>Descendants of these classes are not supported.</p>
|
||
|
*/
|
||
|
public class RemoteViews implements Parcelable, Filter {
|
||
|
|
||
|
private static final String LOG_TAG = "RemoteViews";
|
||
|
|
||
|
/** The intent extra for whether the view whose checked state changed is currently checked. */
|
||
|
public static final String EXTRA_CHECKED = "android.widget.extra.CHECKED";
|
||
|
|
||
|
/**
|
||
|
* The intent extra that contains the appWidgetId.
|
||
|
* @hide
|
||
|
*/
|
||
|
static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId";
|
||
|
|
||
|
/**
|
||
|
* The intent extra that contains {@code true} if inflating as dak text theme.
|
||
|
* @hide
|
||
|
*/
|
||
|
static final String EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND = "remoteAdapterOnLightBackground";
|
||
|
|
||
|
/**
|
||
|
* The intent extra that contains the bounds for all shared elements.
|
||
|
*/
|
||
|
public static final String EXTRA_SHARED_ELEMENT_BOUNDS =
|
||
|
"android.widget.extra.SHARED_ELEMENT_BOUNDS";
|
||
|
|
||
|
/**
|
||
|
* Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and
|
||
|
* {@link #RemoteViews(RemoteViews, RemoteViews)}.
|
||
|
*/
|
||
|
private static final int MAX_NESTED_VIEWS = 10;
|
||
|
|
||
|
/**
|
||
|
* Maximum number of RemoteViews that can be specified in constructor.
|
||
|
*/
|
||
|
private static final int MAX_INIT_VIEW_COUNT = 16;
|
||
|
|
||
|
// The unique identifiers for each custom {@link Action}.
|
||
|
private static final int SET_ON_CLICK_RESPONSE_TAG = 1;
|
||
|
private static final int REFLECTION_ACTION_TAG = 2;
|
||
|
private static final int SET_DRAWABLE_TINT_TAG = 3;
|
||
|
private static final int VIEW_GROUP_ACTION_ADD_TAG = 4;
|
||
|
private static final int VIEW_CONTENT_NAVIGATION_TAG = 5;
|
||
|
private static final int SET_EMPTY_VIEW_ACTION_TAG = 6;
|
||
|
private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7;
|
||
|
private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8;
|
||
|
private static final int SET_REMOTE_VIEW_ADAPTER_INTENT_TAG = 10;
|
||
|
private static final int TEXT_VIEW_DRAWABLE_ACTION_TAG = 11;
|
||
|
private static final int BITMAP_REFLECTION_ACTION_TAG = 12;
|
||
|
private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13;
|
||
|
private static final int VIEW_PADDING_ACTION_TAG = 14;
|
||
|
private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18;
|
||
|
private static final int LAYOUT_PARAM_ACTION_TAG = 19;
|
||
|
private static final int SET_RIPPLE_DRAWABLE_COLOR_TAG = 21;
|
||
|
private static final int SET_INT_TAG_TAG = 22;
|
||
|
private static final int REMOVE_FROM_PARENT_ACTION_TAG = 23;
|
||
|
private static final int RESOURCE_REFLECTION_ACTION_TAG = 24;
|
||
|
private static final int COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG = 25;
|
||
|
private static final int SET_COMPOUND_BUTTON_CHECKED_TAG = 26;
|
||
|
private static final int SET_RADIO_GROUP_CHECKED = 27;
|
||
|
private static final int SET_VIEW_OUTLINE_RADIUS_TAG = 28;
|
||
|
private static final int SET_ON_CHECKED_CHANGE_RESPONSE_TAG = 29;
|
||
|
private static final int NIGHT_MODE_REFLECTION_ACTION_TAG = 30;
|
||
|
private static final int SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG = 31;
|
||
|
private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32;
|
||
|
private static final int SET_REMOTE_ADAPTER_TAG = 33;
|
||
|
private static final int SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG = 34;
|
||
|
private static final int SET_DRAW_INSTRUCTION_TAG = 35;
|
||
|
|
||
|
/** @hide **/
|
||
|
@IntDef(prefix = "MARGIN_", value = {
|
||
|
MARGIN_LEFT,
|
||
|
MARGIN_TOP,
|
||
|
MARGIN_RIGHT,
|
||
|
MARGIN_BOTTOM,
|
||
|
MARGIN_START,
|
||
|
MARGIN_END
|
||
|
})
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
public @interface MarginType {}
|
||
|
/** The value will apply to the marginLeft. */
|
||
|
public static final int MARGIN_LEFT = 0;
|
||
|
/** The value will apply to the marginTop. */
|
||
|
public static final int MARGIN_TOP = 1;
|
||
|
/** The value will apply to the marginRight. */
|
||
|
public static final int MARGIN_RIGHT = 2;
|
||
|
/** The value will apply to the marginBottom. */
|
||
|
public static final int MARGIN_BOTTOM = 3;
|
||
|
/** The value will apply to the marginStart. */
|
||
|
public static final int MARGIN_START = 4;
|
||
|
/** The value will apply to the marginEnd. */
|
||
|
public static final int MARGIN_END = 5;
|
||
|
|
||
|
@IntDef(prefix = "VALUE_TYPE_", value = {
|
||
|
VALUE_TYPE_RAW,
|
||
|
VALUE_TYPE_COMPLEX_UNIT,
|
||
|
VALUE_TYPE_RESOURCE,
|
||
|
VALUE_TYPE_ATTRIBUTE
|
||
|
})
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
@interface ValueType {}
|
||
|
static final int VALUE_TYPE_RAW = 1;
|
||
|
static final int VALUE_TYPE_COMPLEX_UNIT = 2;
|
||
|
static final int VALUE_TYPE_RESOURCE = 3;
|
||
|
static final int VALUE_TYPE_ATTRIBUTE = 4;
|
||
|
|
||
|
/** @hide **/
|
||
|
@IntDef(flag = true, value = {
|
||
|
FLAG_REAPPLY_DISALLOWED,
|
||
|
FLAG_WIDGET_IS_COLLECTION_CHILD,
|
||
|
FLAG_USE_LIGHT_BACKGROUND_LAYOUT
|
||
|
})
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
public @interface ApplyFlags {}
|
||
|
/**
|
||
|
* Whether reapply is disallowed on this remoteview. This maybe be true if some actions modify
|
||
|
* the layout in a way that isn't recoverable, since views are being removed.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int FLAG_REAPPLY_DISALLOWED = 1;
|
||
|
/**
|
||
|
* This flag indicates whether this RemoteViews object is being created from a
|
||
|
* RemoteViewsService for use as a child of a widget collection. This flag is used
|
||
|
* to determine whether or not certain features are available, in particular,
|
||
|
* setting on click extras and setting on click pending intents. The former is enabled,
|
||
|
* and the latter disabled when this flag is true.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int FLAG_WIDGET_IS_COLLECTION_CHILD = 2;
|
||
|
/**
|
||
|
* When this flag is set, the views is inflated with {@link #mLightBackgroundLayoutId} instead
|
||
|
* of {link #mLayoutId}
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int FLAG_USE_LIGHT_BACKGROUND_LAYOUT = 4;
|
||
|
|
||
|
/**
|
||
|
* This mask determines which flags are propagated to nested RemoteViews (either added by
|
||
|
* addView, or set as portrait/landscape/sized RemoteViews).
|
||
|
*/
|
||
|
static final int FLAG_MASK_TO_PROPAGATE =
|
||
|
FLAG_WIDGET_IS_COLLECTION_CHILD | FLAG_USE_LIGHT_BACKGROUND_LAYOUT;
|
||
|
|
||
|
/**
|
||
|
* A ReadWriteHelper which has the same behavior as ReadWriteHelper.DEFAULT, but which is
|
||
|
* intentionally a different instance in order to trick Bundle reader so that it doesn't allow
|
||
|
* lazy initialization.
|
||
|
*/
|
||
|
private static final Parcel.ReadWriteHelper ALTERNATIVE_DEFAULT = new Parcel.ReadWriteHelper();
|
||
|
|
||
|
/**
|
||
|
* Used to restrict the views which can be inflated
|
||
|
*
|
||
|
* @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class)
|
||
|
*/
|
||
|
private static final LayoutInflater.Filter INFLATER_FILTER =
|
||
|
(clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
|
||
|
|
||
|
/**
|
||
|
* The maximum waiting time for remote adapter conversion in milliseconds
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 20_000;
|
||
|
|
||
|
/**
|
||
|
* Application that hosts the remote views.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
public ApplicationInfo mApplication;
|
||
|
|
||
|
/**
|
||
|
* The resource ID of the layout file. (Added to the parcel)
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
private int mLayoutId;
|
||
|
|
||
|
/**
|
||
|
* The resource ID of the layout file in dark text mode. (Added to the parcel)
|
||
|
*/
|
||
|
private int mLightBackgroundLayoutId = 0;
|
||
|
|
||
|
/**
|
||
|
* An array of actions to perform on the view tree once it has been
|
||
|
* inflated
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
private ArrayList<Action> mActions;
|
||
|
|
||
|
/**
|
||
|
* Maps bitmaps to unique indicies to avoid Bitmap duplication.
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
private BitmapCache mBitmapCache = new BitmapCache();
|
||
|
|
||
|
/**
|
||
|
* Maps Intent ID to RemoteCollectionItems to avoid duplicate items
|
||
|
*/
|
||
|
private @NonNull RemoteCollectionCache mCollectionCache = new RemoteCollectionCache();
|
||
|
|
||
|
/** Cache of ApplicationInfos used by collection items. */
|
||
|
private ApplicationInfoCache mApplicationInfoCache = new ApplicationInfoCache();
|
||
|
|
||
|
/**
|
||
|
* Indicates whether or not this RemoteViews object is contained as a child of any other
|
||
|
* RemoteViews.
|
||
|
*/
|
||
|
private boolean mIsRoot = true;
|
||
|
|
||
|
/**
|
||
|
* Constants to whether or not this RemoteViews is composed of a landscape and portrait
|
||
|
* RemoteViews.
|
||
|
*/
|
||
|
private static final int MODE_NORMAL = 0;
|
||
|
private static final int MODE_HAS_LANDSCAPE_AND_PORTRAIT = 1;
|
||
|
private static final int MODE_HAS_SIZED_REMOTEVIEWS = 2;
|
||
|
|
||
|
/**
|
||
|
* Used in conjunction with the special constructor
|
||
|
* {@link #RemoteViews(RemoteViews, RemoteViews)} to keep track of the landscape and portrait
|
||
|
* RemoteViews.
|
||
|
*/
|
||
|
private RemoteViews mLandscape = null;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private RemoteViews mPortrait = null;
|
||
|
/**
|
||
|
* List of RemoteViews with their ideal size. There must be at least two if the map is not null.
|
||
|
*
|
||
|
* The smallest remote view is always the last element in the list.
|
||
|
*/
|
||
|
private List<RemoteViews> mSizedRemoteViews = null;
|
||
|
|
||
|
/**
|
||
|
* Ideal size for this RemoteViews.
|
||
|
*
|
||
|
* Only to be used on children views used in a {@link RemoteViews} with
|
||
|
* {@link RemoteViews#hasSizedRemoteViews()}.
|
||
|
*/
|
||
|
private SizeF mIdealSize = null;
|
||
|
|
||
|
@ApplyFlags
|
||
|
private int mApplyFlags = 0;
|
||
|
|
||
|
/**
|
||
|
* Id to use to override the ID of the top-level view in this RemoteViews.
|
||
|
*
|
||
|
* Only used if this RemoteViews is defined from a XML layout value.
|
||
|
*/
|
||
|
private int mViewId = View.NO_ID;
|
||
|
|
||
|
/**
|
||
|
* Id used to uniquely identify a {@link RemoteViews} instance coming from a given provider.
|
||
|
*/
|
||
|
private long mProviderInstanceId = -1;
|
||
|
|
||
|
/** Class cookies of the Parcel this instance was read from. */
|
||
|
private Map<Class, Object> mClassCookies;
|
||
|
|
||
|
/**
|
||
|
* {@link LayoutInflater.Factory2} which will be passed into a {@link LayoutInflater} instance
|
||
|
* used by this class.
|
||
|
*/
|
||
|
@Nullable
|
||
|
private LayoutInflater.Factory2 mLayoutInflaterFactory2;
|
||
|
|
||
|
/**
|
||
|
* Indicates whether this {@link RemoteViews} was instantiated with a {@link DrawInstructions}
|
||
|
* object. {@link DrawInstructions} serves as an alternative protocol for the host process
|
||
|
* to render.
|
||
|
*/
|
||
|
private boolean mHasDrawInstructions;
|
||
|
|
||
|
@Nullable
|
||
|
private SparseArray<PendingIntent> mPendingIntentTemplate;
|
||
|
|
||
|
@Nullable
|
||
|
private SparseArray<Intent> mFillInIntent;
|
||
|
|
||
|
private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
|
||
|
(view, pendingIntent, response) ->
|
||
|
startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
|
||
|
|
||
|
private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>();
|
||
|
|
||
|
/**
|
||
|
* This key is used to perform lookups in sMethods without causing allocations.
|
||
|
*/
|
||
|
private static final MethodKey sLookupKey = new MethodKey();
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setRemoteInputs(@IdRes int viewId, RemoteInput[] remoteInputs) {
|
||
|
mActions.add(new SetRemoteInputsAction(viewId, remoteInputs));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets {@link LayoutInflater.Factory2} to be passed into {@link LayoutInflater} used
|
||
|
* by this class instance. It has to be set before the views are inflated to have any effect.
|
||
|
*
|
||
|
* The factory callbacks will be called on the background thread so the implementation needs
|
||
|
* to be thread safe.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setLayoutInflaterFactory(@Nullable LayoutInflater.Factory2 factory) {
|
||
|
mLayoutInflaterFactory2 = factory;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns currently set {@link LayoutInflater.Factory2}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@Nullable
|
||
|
public LayoutInflater.Factory2 getLayoutInflaterFactory() {
|
||
|
return mLayoutInflaterFactory2;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reduces all images and ensures that they are all below the given sizes.
|
||
|
*
|
||
|
* @param maxWidth the maximum width allowed
|
||
|
* @param maxHeight the maximum height allowed
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void reduceImageSizes(int maxWidth, int maxHeight) {
|
||
|
ArrayList<Bitmap> cache = mBitmapCache.mBitmaps;
|
||
|
for (int i = 0; i < cache.size(); i++) {
|
||
|
Bitmap bitmap = cache.get(i);
|
||
|
cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an integer tag to the view.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setIntTag(@IdRes int viewId, @IdRes int key, int tag) {
|
||
|
addAction(new SetIntTagAction(viewId, key, tag));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set that it is disallowed to reapply another remoteview with the same layout as this view.
|
||
|
* This should be done if an action is destroying the view tree of the base layout.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void addFlags(@ApplyFlags int flags) {
|
||
|
mApplyFlags = mApplyFlags | flags;
|
||
|
|
||
|
int flagsToPropagate = flags & FLAG_MASK_TO_PROPAGATE;
|
||
|
if (flagsToPropagate != 0) {
|
||
|
if (hasSizedRemoteViews()) {
|
||
|
for (RemoteViews remoteView : mSizedRemoteViews) {
|
||
|
remoteView.addFlags(flagsToPropagate);
|
||
|
}
|
||
|
} else if (hasLandscapeAndPortraitLayouts()) {
|
||
|
mLandscape.addFlags(flagsToPropagate);
|
||
|
mPortrait.addFlags(flagsToPropagate);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean hasFlags(@ApplyFlags int flag) {
|
||
|
return (mApplyFlags & flag) == flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stores information related to reflection method lookup.
|
||
|
*/
|
||
|
static class MethodKey {
|
||
|
public Class targetClass;
|
||
|
public Class paramClass;
|
||
|
public String methodName;
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(@Nullable Object o) {
|
||
|
if (!(o instanceof MethodKey)) {
|
||
|
return false;
|
||
|
}
|
||
|
MethodKey p = (MethodKey) o;
|
||
|
return Objects.equals(p.targetClass, targetClass)
|
||
|
&& Objects.equals(p.paramClass, paramClass)
|
||
|
&& Objects.equals(p.methodName, methodName);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return Objects.hashCode(targetClass) ^ Objects.hashCode(paramClass)
|
||
|
^ Objects.hashCode(methodName);
|
||
|
}
|
||
|
|
||
|
public void set(Class targetClass, Class paramClass, String methodName) {
|
||
|
this.targetClass = targetClass;
|
||
|
this.paramClass = paramClass;
|
||
|
this.methodName = methodName;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Stores information related to reflection method lookup result.
|
||
|
*/
|
||
|
static class MethodArgs {
|
||
|
public MethodHandle syncMethod;
|
||
|
public MethodHandle asyncMethod;
|
||
|
public String asyncMethodName;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This annotation indicates that a subclass of View is allowed to be used
|
||
|
* with the {@link RemoteViews} mechanism.
|
||
|
*/
|
||
|
@Target({ ElementType.TYPE })
|
||
|
@Retention(RetentionPolicy.RUNTIME)
|
||
|
public @interface RemoteView {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Exception to send when something goes wrong executing an action
|
||
|
*
|
||
|
*/
|
||
|
public static class ActionException extends RuntimeException {
|
||
|
public ActionException(Exception ex) {
|
||
|
super(ex);
|
||
|
}
|
||
|
public ActionException(String message) {
|
||
|
super(message);
|
||
|
}
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public ActionException(Throwable t) {
|
||
|
super(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handler for view interactions (such as clicks) within a RemoteViews.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public interface InteractionHandler {
|
||
|
/**
|
||
|
* Invoked when the user performs an interaction on the View.
|
||
|
*
|
||
|
* @param view the View with which the user interacted
|
||
|
* @param pendingIntent the base PendingIntent associated with the view
|
||
|
* @param response the response to the interaction, which knows how to fill in the
|
||
|
* attached PendingIntent
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
boolean onInteraction(
|
||
|
View view,
|
||
|
PendingIntent pendingIntent,
|
||
|
RemoteResponse response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Base class for all actions that can be performed on an
|
||
|
* inflated view.
|
||
|
*
|
||
|
* SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
|
||
|
*/
|
||
|
private abstract static class Action {
|
||
|
@IdRes
|
||
|
@UnsupportedAppUsage
|
||
|
int mViewId;
|
||
|
|
||
|
public abstract void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException;
|
||
|
|
||
|
public static final int MERGE_REPLACE = 0;
|
||
|
public static final int MERGE_APPEND = 1;
|
||
|
public static final int MERGE_IGNORE = 2;
|
||
|
|
||
|
public void setHierarchyRootData(HierarchyRootData root) {
|
||
|
// Do nothing
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
public int mergeBehavior() {
|
||
|
return MERGE_REPLACE;
|
||
|
}
|
||
|
|
||
|
public abstract int getActionTag();
|
||
|
|
||
|
public String getUniqueKey() {
|
||
|
return (getActionTag() + "_" + mViewId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is called on the background thread. It should perform any non-ui computations
|
||
|
* and return the final action which will run on the UI thread.
|
||
|
* Override this if some of the tasks can be performed async.
|
||
|
*/
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public boolean prefersAsyncApply() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** See {@link RemoteViews#visitUris(Consumer)}. **/
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
// Nothing to visit by default.
|
||
|
}
|
||
|
|
||
|
public abstract void writeToParcel(Parcel dest, int flags);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Action class used during async inflation of RemoteViews. Subclasses are not parcelable.
|
||
|
*/
|
||
|
private abstract static class RuntimeAction extends Action {
|
||
|
@Override
|
||
|
public final int getActionTag() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public final void writeToParcel(Parcel dest, int flags) {
|
||
|
throw new UnsupportedOperationException();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Constant used during async execution. It is not parcelable.
|
||
|
private static final Action ACTION_NOOP = new RuntimeAction() {
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { }
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Merges the passed RemoteViews actions with this RemoteViews actions according to
|
||
|
* action-specific merge rules.
|
||
|
*
|
||
|
* @param newRv
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
public void mergeRemoteViews(RemoteViews newRv) {
|
||
|
if (newRv == null) return;
|
||
|
// We first copy the new RemoteViews, as the process of merging modifies the way the actions
|
||
|
// reference the bitmap cache. We don't want to modify the object as it may need to
|
||
|
// be merged and applied multiple times.
|
||
|
RemoteViews copy = new RemoteViews(newRv);
|
||
|
|
||
|
HashMap<String, Action> map = new HashMap<String, Action>();
|
||
|
if (mActions == null) {
|
||
|
mActions = new ArrayList<Action>();
|
||
|
}
|
||
|
|
||
|
int count = mActions.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
Action a = mActions.get(i);
|
||
|
map.put(a.getUniqueKey(), a);
|
||
|
}
|
||
|
|
||
|
ArrayList<Action> newActions = copy.mActions;
|
||
|
if (newActions == null) return;
|
||
|
count = newActions.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
Action a = newActions.get(i);
|
||
|
String key = newActions.get(i).getUniqueKey();
|
||
|
int mergeBehavior = newActions.get(i).mergeBehavior();
|
||
|
if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) {
|
||
|
mActions.remove(map.get(key));
|
||
|
map.remove(key);
|
||
|
}
|
||
|
|
||
|
// If the merge behavior is ignore, we don't bother keeping the extra action
|
||
|
if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) {
|
||
|
mActions.add(a);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Because pruning can remove the need for bitmaps, we reconstruct the caches.
|
||
|
reconstructCaches();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return {@code true} only if this {@code RemoteViews} is a legacy list widget that uses
|
||
|
* {@code Intent} for inflating child entries.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean isLegacyListRemoteViews() {
|
||
|
return mCollectionCache.mIdToUriMapping.size() > 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
|
||
|
* grants will need to be issued to ensure the recipient of this object is able to render its
|
||
|
* contents.
|
||
|
* See b/281044385 for more context and examples about what happens when this isn't done
|
||
|
* correctly.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
if (mActions != null) {
|
||
|
for (int i = 0; i < mActions.size(); i++) {
|
||
|
mActions.get(i).visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
if (mSizedRemoteViews != null) {
|
||
|
for (int i = 0; i < mSizedRemoteViews.size(); i++) {
|
||
|
mSizedRemoteViews.get(i).visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
if (mLandscape != null) {
|
||
|
mLandscape.visitUris(visitor);
|
||
|
}
|
||
|
if (mPortrait != null) {
|
||
|
mPortrait.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* @return True if there is a change
|
||
|
*/
|
||
|
public boolean replaceRemoteCollections(int viewId) {
|
||
|
boolean isActionReplaced = false;
|
||
|
if (mActions != null) {
|
||
|
for (int i = 0; i < mActions.size(); i++) {
|
||
|
Action action = mActions.get(i);
|
||
|
if (action instanceof SetRemoteCollectionItemListAdapterAction itemsAction
|
||
|
&& itemsAction.mViewId == viewId
|
||
|
&& itemsAction.mServiceIntent != null) {
|
||
|
SetRemoteCollectionItemListAdapterAction newCollectionAction =
|
||
|
new SetRemoteCollectionItemListAdapterAction(
|
||
|
itemsAction.mViewId, itemsAction.mServiceIntent);
|
||
|
newCollectionAction.mIntentId = itemsAction.mIntentId;
|
||
|
newCollectionAction.mIsReplacedIntoAction = true;
|
||
|
mActions.set(i, newCollectionAction);
|
||
|
isActionReplaced = true;
|
||
|
} else if (action instanceof SetRemoteViewsAdapterIntent intentAction
|
||
|
&& intentAction.mViewId == viewId) {
|
||
|
mActions.set(i, new SetRemoteCollectionItemListAdapterAction(
|
||
|
intentAction.mViewId, intentAction.mIntent));
|
||
|
isActionReplaced = true;
|
||
|
} else if (action instanceof ViewGroupActionAdd groupAction
|
||
|
&& groupAction.mNestedViews != null) {
|
||
|
isActionReplaced |= groupAction.mNestedViews.replaceRemoteCollections(viewId);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (mSizedRemoteViews != null) {
|
||
|
for (int i = 0; i < mSizedRemoteViews.size(); i++) {
|
||
|
isActionReplaced |= mSizedRemoteViews.get(i).replaceRemoteCollections(viewId);
|
||
|
}
|
||
|
}
|
||
|
if (mLandscape != null) {
|
||
|
isActionReplaced |= mLandscape.replaceRemoteCollections(viewId);
|
||
|
}
|
||
|
if (mPortrait != null) {
|
||
|
isActionReplaced |= mPortrait.replaceRemoteCollections(viewId);
|
||
|
}
|
||
|
|
||
|
return isActionReplaced;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return True if has set remote adapter using service intent
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean hasLegacyLists() {
|
||
|
if (mActions != null) {
|
||
|
for (int i = 0; i < mActions.size(); i++) {
|
||
|
Action action = mActions.get(i);
|
||
|
if ((action instanceof SetRemoteCollectionItemListAdapterAction itemsAction
|
||
|
&& itemsAction.mServiceIntent != null)
|
||
|
|| (action instanceof SetRemoteViewsAdapterIntent intentAction
|
||
|
&& intentAction.mIntent != null)
|
||
|
|| (action instanceof ViewGroupActionAdd groupAction
|
||
|
&& groupAction.mNestedViews != null
|
||
|
&& groupAction.mNestedViews.hasLegacyLists())) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (mSizedRemoteViews != null) {
|
||
|
for (int i = 0; i < mSizedRemoteViews.size(); i++) {
|
||
|
if (mSizedRemoteViews.get(i).hasLegacyLists()) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (mLandscape != null && mLandscape.hasLegacyLists()) {
|
||
|
return true;
|
||
|
}
|
||
|
return mPortrait != null && mPortrait.hasLegacyLists();
|
||
|
}
|
||
|
|
||
|
private static void visitIconUri(Icon icon, @NonNull Consumer<Uri> visitor) {
|
||
|
if (icon != null && (icon.getType() == Icon.TYPE_URI
|
||
|
|| icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
|
||
|
visitor.accept(icon.getUri());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class RemoteViewsContextWrapper extends ContextWrapper {
|
||
|
private final Context mContextForResources;
|
||
|
|
||
|
RemoteViewsContextWrapper(Context context, Context contextForResources) {
|
||
|
super(context);
|
||
|
mContextForResources = contextForResources;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Resources getResources() {
|
||
|
return mContextForResources.getResources();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Resources.Theme getTheme() {
|
||
|
return mContextForResources.getTheme();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getPackageName() {
|
||
|
return mContextForResources.getPackageName();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public UserHandle getUser() {
|
||
|
return mContextForResources.getUser();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getUserId() {
|
||
|
return mContextForResources.getUserId();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean isRestricted() {
|
||
|
// Override isRestricted and direct to resource's implementation. The isRestricted is
|
||
|
// used for determining the risky resources loading, e.g. fonts, thus direct to context
|
||
|
// for resource.
|
||
|
return mContextForResources.isRestricted();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetEmptyView extends Action {
|
||
|
int mEmptyViewId;
|
||
|
|
||
|
SetEmptyView(@IdRes int viewId, @IdRes int emptyViewId) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mEmptyViewId = emptyViewId;
|
||
|
}
|
||
|
|
||
|
SetEmptyView(Parcel in) {
|
||
|
this.mViewId = in.readInt();
|
||
|
this.mEmptyViewId = in.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
out.writeInt(this.mViewId);
|
||
|
out.writeInt(this.mEmptyViewId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View view = root.findViewById(mViewId);
|
||
|
if (!(view instanceof AdapterView<?>)) return;
|
||
|
|
||
|
AdapterView<?> adapterView = (AdapterView<?>) view;
|
||
|
|
||
|
final View emptyView = root.findViewById(mEmptyViewId);
|
||
|
if (emptyView == null) return;
|
||
|
|
||
|
adapterView.setEmptyView(emptyView);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_EMPTY_VIEW_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetPendingIntentTemplate extends Action {
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
PendingIntent mPendingIntentTemplate;
|
||
|
|
||
|
public SetPendingIntentTemplate(@IdRes int id, PendingIntent pendingIntentTemplate) {
|
||
|
this.mViewId = id;
|
||
|
this.mPendingIntentTemplate = pendingIntentTemplate;
|
||
|
}
|
||
|
|
||
|
public SetPendingIntentTemplate(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mPendingIntentTemplate = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
PendingIntent.writePendingIntentOrNullToParcel(mPendingIntentTemplate, dest);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
// If the view isn't an AdapterView, setting a PendingIntent template doesn't make sense
|
||
|
if (target instanceof AdapterView<?>) {
|
||
|
AdapterView<?> av = (AdapterView<?>) target;
|
||
|
// The PendingIntent template is stored in the view's tag.
|
||
|
OnItemClickListener listener = (parent, view, position, id) -> {
|
||
|
RemoteResponse response = findRemoteResponseTag(view);
|
||
|
if (response != null) {
|
||
|
response.handleViewInteraction(view, params.handler);
|
||
|
}
|
||
|
};
|
||
|
av.setOnItemClickListener(listener);
|
||
|
av.setTag(mPendingIntentTemplate);
|
||
|
} else {
|
||
|
Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" +
|
||
|
"an AdapterView (id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private RemoteResponse findRemoteResponseTag(@Nullable View rootView) {
|
||
|
if (rootView == null) return null;
|
||
|
|
||
|
ArrayDeque<View> viewsToCheck = new ArrayDeque<>();
|
||
|
viewsToCheck.addLast(rootView);
|
||
|
|
||
|
while (!viewsToCheck.isEmpty()) {
|
||
|
View view = viewsToCheck.removeFirst();
|
||
|
Object tag = view.getTag(R.id.fillInIntent);
|
||
|
if (tag instanceof RemoteResponse) return (RemoteResponse) tag;
|
||
|
if (!(view instanceof ViewGroup)) continue;
|
||
|
|
||
|
ViewGroup viewGroup = (ViewGroup) view;
|
||
|
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
||
|
viewsToCheck.addLast(viewGroup.getChildAt(i));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_PENDING_INTENT_TEMPLATE_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mPendingIntentTemplate.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cache of {@link ApplicationInfo}s that can be used to ensure that the same
|
||
|
* {@link ApplicationInfo} instance is used throughout the RemoteViews.
|
||
|
*/
|
||
|
private static class ApplicationInfoCache {
|
||
|
private final Map<Pair<String, Integer>, ApplicationInfo> mPackageUserToApplicationInfo;
|
||
|
|
||
|
ApplicationInfoCache() {
|
||
|
mPackageUserToApplicationInfo = new ArrayMap<>();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds the {@link ApplicationInfo} to the cache if it's not present. Returns either the
|
||
|
* provided {@code applicationInfo} or a previously added value with the same package name
|
||
|
* and uid.
|
||
|
*/
|
||
|
@Nullable
|
||
|
ApplicationInfo getOrPut(@Nullable ApplicationInfo applicationInfo) {
|
||
|
Pair<String, Integer> key = getPackageUserKey(applicationInfo);
|
||
|
if (key == null) return null;
|
||
|
return mPackageUserToApplicationInfo.computeIfAbsent(key, ignored -> applicationInfo);
|
||
|
}
|
||
|
|
||
|
/** Puts the {@link ApplicationInfo} in the cache, replacing any previously stored value. */
|
||
|
void put(@Nullable ApplicationInfo applicationInfo) {
|
||
|
Pair<String, Integer> key = getPackageUserKey(applicationInfo);
|
||
|
if (key == null) return;
|
||
|
mPackageUserToApplicationInfo.put(key, applicationInfo);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the currently stored {@link ApplicationInfo} from the cache matching
|
||
|
* {@code applicationInfo}, or null if there wasn't any.
|
||
|
*/
|
||
|
@Nullable ApplicationInfo get(@Nullable ApplicationInfo applicationInfo) {
|
||
|
Pair<String, Integer> key = getPackageUserKey(applicationInfo);
|
||
|
if (key == null) return null;
|
||
|
return mPackageUserToApplicationInfo.get(key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class SetRemoteCollectionItemListAdapterAction extends Action {
|
||
|
private @Nullable RemoteCollectionItems mItems;
|
||
|
final Intent mServiceIntent;
|
||
|
int mIntentId = -1;
|
||
|
boolean mIsReplacedIntoAction = false;
|
||
|
|
||
|
SetRemoteCollectionItemListAdapterAction(@IdRes int id,
|
||
|
@NonNull RemoteCollectionItems items) {
|
||
|
mViewId = id;
|
||
|
items.setHierarchyRootData(getHierarchyRootData());
|
||
|
mItems = items;
|
||
|
mServiceIntent = null;
|
||
|
}
|
||
|
|
||
|
SetRemoteCollectionItemListAdapterAction(@IdRes int id, Intent intent) {
|
||
|
mViewId = id;
|
||
|
mItems = null;
|
||
|
mServiceIntent = intent;
|
||
|
}
|
||
|
|
||
|
SetRemoteCollectionItemListAdapterAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mIntentId = parcel.readInt();
|
||
|
mIsReplacedIntoAction = parcel.readBoolean();
|
||
|
mServiceIntent = parcel.readTypedObject(Intent.CREATOR);
|
||
|
mItems = mServiceIntent != null
|
||
|
? null
|
||
|
: new RemoteCollectionItems(parcel, getHierarchyRootData());
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setHierarchyRootData(HierarchyRootData rootData) {
|
||
|
if (mItems != null) {
|
||
|
mItems.setHierarchyRootData(rootData);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (mIntentId != -1) {
|
||
|
// Set the root data for items in the cache instead
|
||
|
mCollectionCache.setHierarchyDataForId(mIntentId, rootData);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mIntentId);
|
||
|
dest.writeBoolean(mIsReplacedIntoAction);
|
||
|
dest.writeTypedObject(mServiceIntent, flags);
|
||
|
if (mItems != null) {
|
||
|
mItems.writeToParcel(dest, flags, /* attached= */ true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
RemoteCollectionItems items = mIntentId == -1
|
||
|
? mItems == null
|
||
|
? new RemoteCollectionItems.Builder().build()
|
||
|
: mItems
|
||
|
: mCollectionCache.getItemsForId(mIntentId);
|
||
|
|
||
|
// Ensure that we are applying to an AppWidget root
|
||
|
if (!(rootParent instanceof AppWidgetHostView)) {
|
||
|
Log.e(LOG_TAG, "setRemoteAdapter can only be used for "
|
||
|
+ "AppWidgets (root id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!(target instanceof AdapterView)) {
|
||
|
Log.e(LOG_TAG, "Cannot call setRemoteAdapter on a view which is not "
|
||
|
+ "an AdapterView (id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
AdapterView adapterView = (AdapterView) target;
|
||
|
Adapter adapter = adapterView.getAdapter();
|
||
|
// We can reuse the adapter if it's a RemoteCollectionItemsAdapter and the view type
|
||
|
// count hasn't increased. Note that AbsListView allocates a fixed size array for view
|
||
|
// recycling in setAdapter, so we must call setAdapter again if the number of view types
|
||
|
// increases.
|
||
|
if (adapter instanceof RemoteCollectionItemsAdapter
|
||
|
&& adapter.getViewTypeCount() >= items.getViewTypeCount()) {
|
||
|
try {
|
||
|
((RemoteCollectionItemsAdapter) adapter).setData(
|
||
|
items, params.handler, params.colorResources);
|
||
|
} catch (Throwable throwable) {
|
||
|
// setData should never failed with the validation in the items builder, but if
|
||
|
// it does, catch and rethrow.
|
||
|
throw new ActionException(throwable);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
adapterView.setAdapter(new RemoteCollectionItemsAdapter(items,
|
||
|
params.handler, params.colorResources));
|
||
|
} catch (Throwable throwable) {
|
||
|
// This could throw if the AdapterView somehow doesn't accept BaseAdapter due to
|
||
|
// a type error.
|
||
|
throw new ActionException(throwable);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getUniqueKey() {
|
||
|
return (SET_REMOTE_ADAPTER_TAG + "_" + mViewId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
if (mIntentId != -1 || mItems == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mItems.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The maximum size for RemoteViews with converted RemoteCollectionItemsAdapter.
|
||
|
* When converting RemoteViewsAdapter to RemoteCollectionItemsAdapter, we want to put size
|
||
|
* limits on each unique RemoteCollectionItems in order to not exceed the transaction size limit
|
||
|
* for each parcel (typically 1 MB). We leave a certain ratio of the maximum size as a buffer
|
||
|
* for missing calculations of certain parameters (e.g. writing a RemoteCollectionItems to the
|
||
|
* parcel will write its Id array as well, but that is missing when writing itschild RemoteViews
|
||
|
* directly to the parcel as we did in RemoteViewsService)
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
private static final int MAX_SINGLE_PARCEL_SIZE = (int) (1_000_000 * 0.8);
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public CompletableFuture<Void> collectAllIntents() {
|
||
|
return mCollectionCache.collectAllIntentsNoComplete(this);
|
||
|
}
|
||
|
|
||
|
private class RemoteCollectionCache {
|
||
|
private final SparseArray<String> mIdToUriMapping = new SparseArray<>();
|
||
|
private final Map<String, RemoteCollectionItems> mUriToCollectionMapping = new HashMap<>();
|
||
|
|
||
|
RemoteCollectionCache() { }
|
||
|
|
||
|
RemoteCollectionCache(RemoteCollectionCache src) {
|
||
|
for (int i = 0; i < src.mIdToUriMapping.size(); i++) {
|
||
|
String uri = src.mIdToUriMapping.valueAt(i);
|
||
|
mIdToUriMapping.put(src.mIdToUriMapping.keyAt(i), uri);
|
||
|
mUriToCollectionMapping.put(uri, src.mUriToCollectionMapping.get(uri));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
RemoteCollectionCache(Parcel in) {
|
||
|
int cacheSize = in.readInt();
|
||
|
HierarchyRootData currentRootData = new HierarchyRootData(mBitmapCache,
|
||
|
this,
|
||
|
mApplicationInfoCache,
|
||
|
mClassCookies);
|
||
|
for (int i = 0; i < cacheSize; i++) {
|
||
|
int intentId = in.readInt();
|
||
|
String intentUri = in.readString8();
|
||
|
RemoteCollectionItems items = new RemoteCollectionItems(in, currentRootData);
|
||
|
mIdToUriMapping.put(intentId, intentUri);
|
||
|
mUriToCollectionMapping.put(intentUri, items);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void setHierarchyDataForId(int intentId, HierarchyRootData data) {
|
||
|
String uri = mIdToUriMapping.get(intentId);
|
||
|
if (mUriToCollectionMapping.get(uri) == null) {
|
||
|
Log.e(LOG_TAG, "Error setting hierarchy data for id=" + intentId);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RemoteCollectionItems items = mUriToCollectionMapping.get(uri);
|
||
|
items.setHierarchyRootData(data);
|
||
|
}
|
||
|
|
||
|
RemoteCollectionItems getItemsForId(int intentId) {
|
||
|
String uri = mIdToUriMapping.get(intentId);
|
||
|
return mUriToCollectionMapping.get(uri);
|
||
|
}
|
||
|
|
||
|
public @NonNull CompletableFuture<Void> collectAllIntentsNoComplete(
|
||
|
@NonNull RemoteViews inViews) {
|
||
|
SparseArray<Intent> idToIntentMapping = new SparseArray<>();
|
||
|
// Collect the number of uinque Intent (which is equal to the number of new connections
|
||
|
// to make) for size allocation and exclude certain collections from being written to
|
||
|
// the parcel to better estimate the space left for reallocation.
|
||
|
collectAllIntentsInternal(inViews, idToIntentMapping);
|
||
|
|
||
|
// Calculate the individual size here
|
||
|
int numOfIntents = idToIntentMapping.size();
|
||
|
if (numOfIntents == 0) {
|
||
|
Log.e(LOG_TAG, "Possibly notifying updates for nonexistent view Id");
|
||
|
return CompletableFuture.completedFuture(null);
|
||
|
}
|
||
|
|
||
|
Parcel sizeTestParcel = Parcel.obtain();
|
||
|
// Write self RemoteViews to the parcel, which includes the actions/bitmaps/collection
|
||
|
// cache to see how much space is left for the RemoteCollectionItems that are to be
|
||
|
// updated.
|
||
|
RemoteViews.this.writeToParcel(sizeTestParcel,
|
||
|
/* flags= */ 0,
|
||
|
/* intentsToIgnore= */ idToIntentMapping);
|
||
|
int remainingSize = MAX_SINGLE_PARCEL_SIZE - sizeTestParcel.dataSize();
|
||
|
sizeTestParcel.recycle();
|
||
|
|
||
|
int individualSize = remainingSize < 0
|
||
|
? 0
|
||
|
: remainingSize / numOfIntents;
|
||
|
|
||
|
return connectAllUniqueIntents(individualSize, idToIntentMapping);
|
||
|
}
|
||
|
|
||
|
private void collectAllIntentsInternal(@NonNull RemoteViews inViews,
|
||
|
@NonNull SparseArray<Intent> idToIntentMapping) {
|
||
|
if (inViews.hasSizedRemoteViews()) {
|
||
|
for (RemoteViews remoteViews : inViews.mSizedRemoteViews) {
|
||
|
collectAllIntentsInternal(remoteViews, idToIntentMapping);
|
||
|
}
|
||
|
} else if (inViews.hasLandscapeAndPortraitLayouts()) {
|
||
|
collectAllIntentsInternal(inViews.mLandscape, idToIntentMapping);
|
||
|
collectAllIntentsInternal(inViews.mPortrait, idToIntentMapping);
|
||
|
} else if (inViews.mActions != null) {
|
||
|
for (Action action : inViews.mActions) {
|
||
|
if (action instanceof SetRemoteCollectionItemListAdapterAction rca) {
|
||
|
// Deal with the case where the intent is replaced into the action list
|
||
|
if (rca.mIntentId != -1 && !rca.mIsReplacedIntoAction) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (rca.mIntentId != -1 && rca.mIsReplacedIntoAction) {
|
||
|
rca.mIsReplacedIntoAction = false;
|
||
|
|
||
|
// Avoid redundant connections for the same intent. Also making sure
|
||
|
// that the number of connections we are making is always equal to the
|
||
|
// nmuber of unique intents that are being used for the updates.
|
||
|
if (idToIntentMapping.contains(rca.mIntentId)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
idToIntentMapping.put(rca.mIntentId, rca.mServiceIntent);
|
||
|
rca.mItems = null;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Differentiate between the normal collection actions and the ones with
|
||
|
// intents.
|
||
|
if (rca.mServiceIntent != null) {
|
||
|
final String uri = rca.mServiceIntent.toUri(0);
|
||
|
int index = mIdToUriMapping.indexOfValueByValue(uri);
|
||
|
if (index == -1) {
|
||
|
int newIntentId = mIdToUriMapping.size();
|
||
|
rca.mIntentId = newIntentId;
|
||
|
mIdToUriMapping.put(newIntentId, uri);
|
||
|
} else {
|
||
|
rca.mIntentId = mIdToUriMapping.keyAt(index);
|
||
|
rca.mItems = null;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
idToIntentMapping.put(rca.mIntentId, rca.mServiceIntent);
|
||
|
rca.mItems = null;
|
||
|
} else {
|
||
|
for (RemoteViews views : rca.mItems.mViews) {
|
||
|
collectAllIntentsInternal(views, idToIntentMapping);
|
||
|
}
|
||
|
}
|
||
|
} else if (action instanceof ViewGroupActionAdd vgaa
|
||
|
&& vgaa.mNestedViews != null) {
|
||
|
collectAllIntentsInternal(vgaa.mNestedViews, idToIntentMapping);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private @NonNull CompletableFuture<Void> connectAllUniqueIntents(int individualSize,
|
||
|
@NonNull SparseArray<Intent> idToIntentMapping) {
|
||
|
List<CompletableFuture<Void>> intentFutureList = new ArrayList<>();
|
||
|
for (int i = 0; i < idToIntentMapping.size(); i++) {
|
||
|
String currentIntentUri = mIdToUriMapping.get(idToIntentMapping.keyAt(i));
|
||
|
Intent currentIntent = idToIntentMapping.valueAt(i);
|
||
|
intentFutureList.add(getItemsFutureFromIntentWithTimeout(currentIntent,
|
||
|
individualSize)
|
||
|
.thenAccept(items -> {
|
||
|
items.setHierarchyRootData(getHierarchyRootData());
|
||
|
mUriToCollectionMapping.put(currentIntentUri, items);
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
return CompletableFuture.allOf(intentFutureList.toArray(CompletableFuture[]::new));
|
||
|
}
|
||
|
|
||
|
private static CompletableFuture<RemoteCollectionItems> getItemsFutureFromIntentWithTimeout(
|
||
|
Intent intent, int individualSize) {
|
||
|
if (intent == null) {
|
||
|
Log.e(LOG_TAG, "Null intent received when generating adapter future");
|
||
|
return CompletableFuture.completedFuture(new RemoteCollectionItems
|
||
|
.Builder().build());
|
||
|
}
|
||
|
|
||
|
final Context context = ActivityThread.currentApplication();
|
||
|
|
||
|
final CompletableFuture<RemoteCollectionItems> result = new CompletableFuture<>();
|
||
|
context.bindService(intent, Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE),
|
||
|
result.defaultExecutor(), new ServiceConnection() {
|
||
|
@Override
|
||
|
public void onServiceConnected(ComponentName componentName,
|
||
|
IBinder iBinder) {
|
||
|
RemoteCollectionItems items;
|
||
|
try {
|
||
|
items = IRemoteViewsFactory.Stub.asInterface(iBinder)
|
||
|
.getRemoteCollectionItems(individualSize);
|
||
|
} catch (RemoteException re) {
|
||
|
items = new RemoteCollectionItems.Builder().build();
|
||
|
Log.e(LOG_TAG, "Error getting collection items from the"
|
||
|
+ " factory", re);
|
||
|
} finally {
|
||
|
context.unbindService(this);
|
||
|
}
|
||
|
|
||
|
result.complete(items);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onServiceDisconnected(ComponentName componentName) { }
|
||
|
});
|
||
|
|
||
|
result.completeOnTimeout(
|
||
|
new RemoteCollectionItems.Builder().build(),
|
||
|
MAX_ADAPTER_CONVERSION_WAITING_TIME_MS, TimeUnit.MILLISECONDS);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel out, int flags,
|
||
|
@Nullable SparseArray<Intent> intentsToIgnore) {
|
||
|
out.writeInt(mIdToUriMapping.size());
|
||
|
for (int i = 0; i < mIdToUriMapping.size(); i++) {
|
||
|
int currentIntentId = mIdToUriMapping.keyAt(i);
|
||
|
if (intentsToIgnore != null && intentsToIgnore.contains(currentIntentId)) {
|
||
|
// Skip writing collections that are to be updated in the following steps to
|
||
|
// better estimate the RemoteViews size.
|
||
|
continue;
|
||
|
}
|
||
|
out.writeInt(currentIntentId);
|
||
|
String intentUri = mIdToUriMapping.valueAt(i);
|
||
|
out.writeString8(intentUri);
|
||
|
mUriToCollectionMapping.get(intentUri).writeToParcel(out, flags, true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class SetRemoteViewsAdapterIntent extends Action {
|
||
|
Intent mIntent;
|
||
|
boolean mIsAsync = false;
|
||
|
|
||
|
public SetRemoteViewsAdapterIntent(@IdRes int id, Intent intent) {
|
||
|
this.mViewId = id;
|
||
|
this.mIntent = intent;
|
||
|
}
|
||
|
|
||
|
public SetRemoteViewsAdapterIntent(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mIntent = parcel.readTypedObject(Intent.CREATOR);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeTypedObject(mIntent, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
// Ensure that we are applying to an AppWidget root
|
||
|
if (!(rootParent instanceof AppWidgetHostView)) {
|
||
|
Log.e(LOG_TAG, "setRemoteAdapter can only be used for "
|
||
|
+ "AppWidgets (root id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Ensure that we are calling setRemoteAdapter on an AdapterView that supports it
|
||
|
if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) {
|
||
|
Log.e(LOG_TAG, "Cannot setRemoteAdapter on a view which is not "
|
||
|
+ "an AbsListView or AdapterViewAnimator (id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent
|
||
|
// RemoteViewsService
|
||
|
AppWidgetHostView host = (AppWidgetHostView) rootParent;
|
||
|
mIntent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId())
|
||
|
.putExtra(EXTRA_REMOTEADAPTER_ON_LIGHT_BACKGROUND,
|
||
|
hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT));
|
||
|
|
||
|
if (target instanceof AbsListView) {
|
||
|
AbsListView v = (AbsListView) target;
|
||
|
v.setRemoteViewsAdapter(mIntent, mIsAsync);
|
||
|
v.setRemoteViewsInteractionHandler(params.handler);
|
||
|
} else if (target instanceof AdapterViewAnimator) {
|
||
|
AdapterViewAnimator v = (AdapterViewAnimator) target;
|
||
|
v.setRemoteViewsAdapter(mIntent, mIsAsync);
|
||
|
v.setRemoteViewsOnClickHandler(params.handler);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(mViewId, mIntent);
|
||
|
copy.mIsAsync = true;
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_REMOTE_VIEW_ADAPTER_INTENT_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mIntent.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
|
||
|
* to launch the provided {@link PendingIntent}.
|
||
|
*/
|
||
|
private class SetOnClickResponse extends Action {
|
||
|
final RemoteResponse mResponse;
|
||
|
|
||
|
SetOnClickResponse(@IdRes int id, RemoteResponse response) {
|
||
|
this.mViewId = id;
|
||
|
this.mResponse = response;
|
||
|
}
|
||
|
|
||
|
SetOnClickResponse(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mResponse = new RemoteResponse();
|
||
|
mResponse.readFromParcel(parcel);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
mResponse.writeToParcel(dest, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
if (hasDrawInstructions() && root instanceof RemoteComposePlayer) {
|
||
|
return;
|
||
|
}
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
if (mResponse.mPendingIntent != null) {
|
||
|
// If the view is an AdapterView, setting a PendingIntent on click doesn't make
|
||
|
// much sense, do they mean to set a PendingIntent template for the
|
||
|
// AdapterView's children?
|
||
|
if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
|
||
|
Log.w(LOG_TAG, "Cannot SetOnClickResponse for collection item "
|
||
|
+ "(id: " + mViewId + ")");
|
||
|
ApplicationInfo appInfo = root.getContext().getApplicationInfo();
|
||
|
|
||
|
// We let this slide for HC and ICS so as to not break compatibility. It should
|
||
|
// have been disabled from the outset, but was left open by accident.
|
||
|
if (appInfo != null
|
||
|
&& appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent);
|
||
|
} else if (mResponse.mFillIntent != null) {
|
||
|
if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
|
||
|
Log.e(LOG_TAG, "The method setOnClickFillInIntent is available "
|
||
|
+ "only from RemoteViewsFactory (ie. on collection items).");
|
||
|
return;
|
||
|
}
|
||
|
if (target == root) {
|
||
|
// Target is a root node of an AdapterView child. Set the response in the tag.
|
||
|
// Actual click handling is done by OnItemClickListener in
|
||
|
// SetPendingIntentTemplate, which uses this tag information.
|
||
|
target.setTagInternal(com.android.internal.R.id.fillInIntent, mResponse);
|
||
|
return;
|
||
|
}
|
||
|
} else {
|
||
|
// No intent to apply, clear the listener and any tags that were previously set.
|
||
|
target.setOnClickListener(null);
|
||
|
target.setTagInternal(R.id.pending_intent_tag, null);
|
||
|
target.setTagInternal(com.android.internal.R.id.fillInIntent, null);
|
||
|
return;
|
||
|
}
|
||
|
target.setOnClickListener(v -> mResponse.handleViewInteraction(v, params.handler));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_ON_CLICK_RESPONSE_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mResponse.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Helper action to configure handwriting delegation via {@link PendingIntent}. */
|
||
|
private class SetOnStylusHandwritingResponse extends Action {
|
||
|
final PendingIntent mPendingIntent;
|
||
|
|
||
|
SetOnStylusHandwritingResponse(@IdRes int id, @Nullable PendingIntent pendingIntent) {
|
||
|
this.mViewId = id;
|
||
|
this.mPendingIntent = pendingIntent;
|
||
|
}
|
||
|
|
||
|
SetOnStylusHandwritingResponse(@NonNull Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
|
||
|
Log.w(LOG_TAG, "Cannot use setOnStylusHandwritingPendingIntent for collection item "
|
||
|
+ "(id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (mPendingIntent != null) {
|
||
|
RemoteResponse response = RemoteResponse.fromPendingIntent(mPendingIntent);
|
||
|
target.setHandwritingDelegatorCallback(
|
||
|
() -> response.handleViewInteraction(target, params.handler));
|
||
|
target.setAllowedHandwritingDelegatePackage(mPendingIntent.getCreatorPackage());
|
||
|
} else {
|
||
|
target.setHandwritingDelegatorCallback(null);
|
||
|
target.setAllowedHandwritingDelegatePackage(null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mPendingIntent.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link android.widget.CompoundButton#setOnCheckedChangeListener(
|
||
|
* android.widget.CompoundButton.OnCheckedChangeListener)}
|
||
|
* to launch the provided {@link PendingIntent}.
|
||
|
*/
|
||
|
private class SetOnCheckedChangeResponse extends Action {
|
||
|
private final RemoteResponse mResponse;
|
||
|
|
||
|
SetOnCheckedChangeResponse(@IdRes int id, RemoteResponse response) {
|
||
|
this.mViewId = id;
|
||
|
this.mResponse = response;
|
||
|
}
|
||
|
|
||
|
SetOnCheckedChangeResponse(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mResponse = new RemoteResponse();
|
||
|
mResponse.readFromParcel(parcel);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
mResponse.writeToParcel(dest, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
if (!(target instanceof CompoundButton)) {
|
||
|
Log.w(LOG_TAG, "setOnCheckedChange methods cannot be used on "
|
||
|
+ "non-CompoundButton child (id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
CompoundButton button = (CompoundButton) target;
|
||
|
|
||
|
if (mResponse.mPendingIntent != null) {
|
||
|
// setOnCheckedChangePendingIntent cannot be used with collection children, which
|
||
|
// must use setOnCheckedChangeFillInIntent instead.
|
||
|
if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
|
||
|
Log.w(LOG_TAG, "Cannot setOnCheckedChangePendingIntent for collection item "
|
||
|
+ "(id: " + mViewId + ")");
|
||
|
return;
|
||
|
}
|
||
|
target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent);
|
||
|
} else if (mResponse.mFillIntent != null) {
|
||
|
if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
|
||
|
Log.e(LOG_TAG, "The method setOnCheckedChangeFillInIntent is available "
|
||
|
+ "only from RemoteViewsFactory (ie. on collection items).");
|
||
|
return;
|
||
|
}
|
||
|
} else {
|
||
|
// No intent to apply, clear any existing listener or tag.
|
||
|
button.setOnCheckedChangeListener(null);
|
||
|
button.setTagInternal(R.id.remote_checked_change_listener_tag, null);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
OnCheckedChangeListener onCheckedChangeListener =
|
||
|
(v, isChecked) -> mResponse.handleViewInteraction(v, params.handler);
|
||
|
button.setTagInternal(R.id.remote_checked_change_listener_tag, onCheckedChangeListener);
|
||
|
button.setOnCheckedChangeListener(onCheckedChangeListener);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_ON_CHECKED_CHANGE_RESPONSE_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mResponse.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide **/
|
||
|
public static Rect getSourceBounds(View v) {
|
||
|
final float appScale = v.getContext().getResources()
|
||
|
.getCompatibilityInfo().applicationScale;
|
||
|
final int[] pos = new int[2];
|
||
|
v.getLocationOnScreen(pos);
|
||
|
|
||
|
final Rect rect = new Rect();
|
||
|
rect.left = (int) (pos[0] * appScale + 0.5f);
|
||
|
rect.top = (int) (pos[1] * appScale + 0.5f);
|
||
|
rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
|
||
|
rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
|
||
|
return rect;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static Class<?> getParameterType(int type) {
|
||
|
switch (type) {
|
||
|
case BaseReflectionAction.BOOLEAN:
|
||
|
return boolean.class;
|
||
|
case BaseReflectionAction.BYTE:
|
||
|
return byte.class;
|
||
|
case BaseReflectionAction.SHORT:
|
||
|
return short.class;
|
||
|
case BaseReflectionAction.INT:
|
||
|
return int.class;
|
||
|
case BaseReflectionAction.LONG:
|
||
|
return long.class;
|
||
|
case BaseReflectionAction.FLOAT:
|
||
|
return float.class;
|
||
|
case BaseReflectionAction.DOUBLE:
|
||
|
return double.class;
|
||
|
case BaseReflectionAction.CHAR:
|
||
|
return char.class;
|
||
|
case BaseReflectionAction.STRING:
|
||
|
return String.class;
|
||
|
case BaseReflectionAction.CHAR_SEQUENCE:
|
||
|
return CharSequence.class;
|
||
|
case BaseReflectionAction.URI:
|
||
|
return Uri.class;
|
||
|
case BaseReflectionAction.BITMAP:
|
||
|
return Bitmap.class;
|
||
|
case BaseReflectionAction.BUNDLE:
|
||
|
return Bundle.class;
|
||
|
case BaseReflectionAction.INTENT:
|
||
|
return Intent.class;
|
||
|
case BaseReflectionAction.COLOR_STATE_LIST:
|
||
|
return ColorStateList.class;
|
||
|
case BaseReflectionAction.ICON:
|
||
|
return Icon.class;
|
||
|
case BaseReflectionAction.BLEND_MODE:
|
||
|
return BlendMode.class;
|
||
|
default:
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static MethodHandle getMethod(View view, String methodName, Class<?> paramType,
|
||
|
boolean async) {
|
||
|
MethodArgs result;
|
||
|
Class<? extends View> klass = view.getClass();
|
||
|
|
||
|
synchronized (sMethods) {
|
||
|
// The key is defined by the view class, param class and method name.
|
||
|
sLookupKey.set(klass, paramType, methodName);
|
||
|
result = sMethods.get(sLookupKey);
|
||
|
|
||
|
if (result == null) {
|
||
|
Method method;
|
||
|
try {
|
||
|
if (paramType == null) {
|
||
|
method = klass.getMethod(methodName);
|
||
|
} else {
|
||
|
method = klass.getMethod(methodName, paramType);
|
||
|
}
|
||
|
if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
|
||
|
throw new ActionException("view: " + klass.getName()
|
||
|
+ " can't use method with RemoteViews: "
|
||
|
+ methodName + getParameters(paramType));
|
||
|
}
|
||
|
|
||
|
result = new MethodArgs();
|
||
|
result.syncMethod = MethodHandles.publicLookup().unreflect(method);
|
||
|
result.asyncMethodName =
|
||
|
method.getAnnotation(RemotableViewMethod.class).asyncImpl();
|
||
|
} catch (NoSuchMethodException | IllegalAccessException ex) {
|
||
|
throw new ActionException("view: " + klass.getName() + " doesn't have method: "
|
||
|
+ methodName + getParameters(paramType));
|
||
|
}
|
||
|
|
||
|
MethodKey key = new MethodKey();
|
||
|
key.set(klass, paramType, methodName);
|
||
|
sMethods.put(key, result);
|
||
|
}
|
||
|
|
||
|
if (!async) {
|
||
|
return result.syncMethod;
|
||
|
}
|
||
|
// Check this so see if async method is implemented or not.
|
||
|
if (result.asyncMethodName.isEmpty()) {
|
||
|
return null;
|
||
|
}
|
||
|
// Async method is lazily loaded. If it is not yet loaded, load now.
|
||
|
if (result.asyncMethod == null) {
|
||
|
MethodType asyncType = result.syncMethod.type()
|
||
|
.dropParameterTypes(0, 1).changeReturnType(Runnable.class);
|
||
|
try {
|
||
|
result.asyncMethod = MethodHandles.publicLookup().findVirtual(
|
||
|
klass, result.asyncMethodName, asyncType);
|
||
|
} catch (NoSuchMethodException | IllegalAccessException ex) {
|
||
|
throw new ActionException("Async implementation declared as "
|
||
|
+ result.asyncMethodName + " but not defined for " + methodName
|
||
|
+ ": public Runnable " + result.asyncMethodName + " ("
|
||
|
+ TextUtils.join(",", asyncType.parameterArray()) + ")");
|
||
|
}
|
||
|
}
|
||
|
return result.asyncMethod;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static String getParameters(Class<?> paramType) {
|
||
|
if (paramType == null) return "()";
|
||
|
return "(" + paramType + ")";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
|
||
|
* on the {@link Drawable} of a given view.
|
||
|
* <p>
|
||
|
* The operation will be performed on the {@link Drawable} returned by the
|
||
|
* target {@link View#getBackground()} by default. If targetBackground is false,
|
||
|
* we assume the target is an {@link ImageView} and try applying the operations
|
||
|
* to {@link ImageView#getDrawable()}.
|
||
|
* <p>
|
||
|
*/
|
||
|
private static class SetDrawableTint extends Action {
|
||
|
boolean mTargetBackground;
|
||
|
@ColorInt int mColorFilter;
|
||
|
PorterDuff.Mode mFilterMode;
|
||
|
|
||
|
SetDrawableTint(@IdRes int id, boolean targetBackground,
|
||
|
@ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) {
|
||
|
this.mViewId = id;
|
||
|
this.mTargetBackground = targetBackground;
|
||
|
this.mColorFilter = colorFilter;
|
||
|
this.mFilterMode = mode;
|
||
|
}
|
||
|
|
||
|
SetDrawableTint(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mTargetBackground = parcel.readInt() != 0;
|
||
|
mColorFilter = parcel.readInt();
|
||
|
mFilterMode = PorterDuff.intToMode(parcel.readInt());
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mTargetBackground ? 1 : 0);
|
||
|
dest.writeInt(mColorFilter);
|
||
|
dest.writeInt(PorterDuff.modeToInt(mFilterMode));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
// Pick the correct drawable to modify for this view
|
||
|
Drawable targetDrawable = null;
|
||
|
if (mTargetBackground) {
|
||
|
targetDrawable = target.getBackground();
|
||
|
} else if (target instanceof ImageView) {
|
||
|
ImageView imageView = (ImageView) target;
|
||
|
targetDrawable = imageView.getDrawable();
|
||
|
}
|
||
|
|
||
|
if (targetDrawable != null) {
|
||
|
targetDrawable.mutate().setColorFilter(mColorFilter, mFilterMode);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_DRAWABLE_TINT_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link RippleDrawable#setColor(ColorStateList)},
|
||
|
* on the {@link Drawable} of a given view.
|
||
|
* <p>
|
||
|
* The operation will be performed on the {@link Drawable} returned by the
|
||
|
* target {@link View#getBackground()}.
|
||
|
* <p>
|
||
|
*/
|
||
|
private class SetRippleDrawableColor extends Action {
|
||
|
ColorStateList mColorStateList;
|
||
|
|
||
|
SetRippleDrawableColor(@IdRes int id, ColorStateList colorStateList) {
|
||
|
this.mViewId = id;
|
||
|
this.mColorStateList = colorStateList;
|
||
|
}
|
||
|
|
||
|
SetRippleDrawableColor(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mColorStateList = parcel.readParcelable(null, android.content.res.ColorStateList.class);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeParcelable(mColorStateList, 0);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
// Pick the correct drawable to modify for this view
|
||
|
Drawable targetDrawable = target.getBackground();
|
||
|
|
||
|
if (targetDrawable instanceof RippleDrawable) {
|
||
|
((RippleDrawable) targetDrawable.mutate()).setColor(mColorStateList);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_RIPPLE_DRAWABLE_COLOR_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @deprecated As RemoteViews may be reapplied frequently, it is preferable to call
|
||
|
* {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change
|
||
|
* unexpectedly.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
private final class ViewContentNavigation extends Action {
|
||
|
final boolean mNext;
|
||
|
|
||
|
ViewContentNavigation(@IdRes int viewId, boolean next) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mNext = next;
|
||
|
}
|
||
|
|
||
|
ViewContentNavigation(Parcel in) {
|
||
|
this.mViewId = in.readInt();
|
||
|
this.mNext = in.readBoolean();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
out.writeInt(this.mViewId);
|
||
|
out.writeBoolean(this.mNext);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View view = root.findViewById(mViewId);
|
||
|
if (view == null) return;
|
||
|
|
||
|
try {
|
||
|
getMethod(view,
|
||
|
mNext ? "showNext" : "showPrevious", null, false /* async */).invoke(view);
|
||
|
} catch (Throwable ex) {
|
||
|
throw new ActionException(ex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public int mergeBehavior() {
|
||
|
return MERGE_IGNORE;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return VIEW_CONTENT_NAVIGATION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class BitmapCache {
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
ArrayList<Bitmap> mBitmaps;
|
||
|
SparseIntArray mBitmapHashes;
|
||
|
int mBitmapMemory = -1;
|
||
|
|
||
|
public BitmapCache() {
|
||
|
mBitmaps = new ArrayList<>();
|
||
|
mBitmapHashes = new SparseIntArray();
|
||
|
}
|
||
|
|
||
|
public BitmapCache(Parcel source) {
|
||
|
mBitmaps = source.createTypedArrayList(Bitmap.CREATOR);
|
||
|
mBitmapHashes = new SparseIntArray();
|
||
|
for (int i = 0; i < mBitmaps.size(); i++) {
|
||
|
Bitmap b = mBitmaps.get(i);
|
||
|
if (b != null) {
|
||
|
mBitmapHashes.put(b.hashCode(), i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public int getBitmapId(Bitmap b) {
|
||
|
if (b == null) {
|
||
|
return -1;
|
||
|
} else {
|
||
|
int hash = b.hashCode();
|
||
|
int hashId = mBitmapHashes.get(hash, -1);
|
||
|
if (hashId != -1) {
|
||
|
return hashId;
|
||
|
} else {
|
||
|
if (b.isMutable()) {
|
||
|
b = b.asShared();
|
||
|
}
|
||
|
mBitmaps.add(b);
|
||
|
mBitmapHashes.put(hash, mBitmaps.size() - 1);
|
||
|
mBitmapMemory = -1;
|
||
|
return (mBitmaps.size() - 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
public Bitmap getBitmapForId(int id) {
|
||
|
if (id == -1 || id >= mBitmaps.size()) {
|
||
|
return null;
|
||
|
}
|
||
|
return mBitmaps.get(id);
|
||
|
}
|
||
|
|
||
|
public void writeBitmapsToParcel(Parcel dest, int flags) {
|
||
|
dest.writeTypedList(mBitmaps, flags);
|
||
|
}
|
||
|
|
||
|
public int getBitmapMemory() {
|
||
|
if (mBitmapMemory < 0) {
|
||
|
mBitmapMemory = 0;
|
||
|
int count = mBitmaps.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
mBitmapMemory += mBitmaps.get(i).getAllocationByteCount();
|
||
|
}
|
||
|
}
|
||
|
return mBitmapMemory;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class BitmapReflectionAction extends Action {
|
||
|
int mBitmapId;
|
||
|
@UnsupportedAppUsage
|
||
|
Bitmap mBitmap;
|
||
|
@UnsupportedAppUsage
|
||
|
String mMethodName;
|
||
|
|
||
|
BitmapReflectionAction(@IdRes int viewId, String methodName, Bitmap bitmap) {
|
||
|
this.mBitmap = bitmap;
|
||
|
this.mViewId = viewId;
|
||
|
this.mMethodName = methodName;
|
||
|
mBitmapId = mBitmapCache.getBitmapId(bitmap);
|
||
|
}
|
||
|
|
||
|
BitmapReflectionAction(Parcel in) {
|
||
|
mViewId = in.readInt();
|
||
|
mMethodName = in.readString8();
|
||
|
mBitmapId = in.readInt();
|
||
|
mBitmap = mBitmapCache.getBitmapForId(mBitmapId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeString8(mMethodName);
|
||
|
dest.writeInt(mBitmapId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
ReflectionAction ra = new ReflectionAction(mViewId, mMethodName,
|
||
|
BaseReflectionAction.BITMAP,
|
||
|
mBitmap);
|
||
|
ra.apply(root, rootParent, params);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setHierarchyRootData(HierarchyRootData rootData) {
|
||
|
mBitmapId = rootData.mBitmapCache.getBitmapId(mBitmap);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return BITMAP_REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Base class for the reflection actions.
|
||
|
*/
|
||
|
private abstract static class BaseReflectionAction extends Action {
|
||
|
static final int BOOLEAN = 1;
|
||
|
static final int BYTE = 2;
|
||
|
static final int SHORT = 3;
|
||
|
static final int INT = 4;
|
||
|
static final int LONG = 5;
|
||
|
static final int FLOAT = 6;
|
||
|
static final int DOUBLE = 7;
|
||
|
static final int CHAR = 8;
|
||
|
static final int STRING = 9;
|
||
|
static final int CHAR_SEQUENCE = 10;
|
||
|
static final int URI = 11;
|
||
|
// BITMAP actions are never stored in the list of actions. They are only used locally
|
||
|
// to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
|
||
|
static final int BITMAP = 12;
|
||
|
static final int BUNDLE = 13;
|
||
|
static final int INTENT = 14;
|
||
|
static final int COLOR_STATE_LIST = 15;
|
||
|
static final int ICON = 16;
|
||
|
static final int BLEND_MODE = 17;
|
||
|
|
||
|
@UnsupportedAppUsage
|
||
|
String mMethodName;
|
||
|
int mType;
|
||
|
|
||
|
BaseReflectionAction(@IdRes int viewId, String methodName, int type) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mMethodName = methodName;
|
||
|
this.mType = type;
|
||
|
}
|
||
|
|
||
|
BaseReflectionAction(Parcel in) {
|
||
|
this.mViewId = in.readInt();
|
||
|
this.mMethodName = in.readString8();
|
||
|
this.mType = in.readInt();
|
||
|
//noinspection ConstantIfStatement
|
||
|
if (false) {
|
||
|
Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.mViewId)
|
||
|
+ " methodName=" + this.mMethodName + " type=" + this.mType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
out.writeInt(this.mViewId);
|
||
|
out.writeString8(this.mMethodName);
|
||
|
out.writeInt(this.mType);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the value to use as parameter for the method.
|
||
|
*
|
||
|
* The view might be passed as {@code null} if the parameter value is requested outside of
|
||
|
* inflation. If the parameter cannot be determined at that time, the method should return
|
||
|
* {@code null} but not raise any exception.
|
||
|
*/
|
||
|
@Nullable
|
||
|
protected abstract Object getParameterValue(@Nullable View view) throws ActionException;
|
||
|
|
||
|
@Override
|
||
|
public final void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View view = root.findViewById(mViewId);
|
||
|
if (view == null) return;
|
||
|
|
||
|
Class<?> param = getParameterType(this.mType);
|
||
|
if (param == null) {
|
||
|
throw new ActionException("bad type: " + this.mType);
|
||
|
}
|
||
|
Object value = getParameterValue(view);
|
||
|
try {
|
||
|
getMethod(view, this.mMethodName, param, false /* async */).invoke(view, value);
|
||
|
} catch (Throwable ex) {
|
||
|
throw new ActionException(ex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public final Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
final View view = root.findViewById(mViewId);
|
||
|
if (view == null) return ACTION_NOOP;
|
||
|
|
||
|
Class<?> param = getParameterType(this.mType);
|
||
|
if (param == null) {
|
||
|
throw new ActionException("bad type: " + this.mType);
|
||
|
}
|
||
|
|
||
|
Object value = getParameterValue(view);
|
||
|
try {
|
||
|
MethodHandle method = getMethod(view, this.mMethodName, param, true /* async */);
|
||
|
// Upload the bitmap to GPU if the parameter is of type Bitmap or Icon.
|
||
|
// Since bitmaps in framework are seldomly modified, this is supposed to accelerate
|
||
|
// the operations.
|
||
|
if (value instanceof Bitmap bitmap) {
|
||
|
bitmap.prepareToDraw();
|
||
|
}
|
||
|
|
||
|
if (value instanceof Icon icon
|
||
|
&& (icon.getType() == Icon.TYPE_BITMAP
|
||
|
|| icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) {
|
||
|
Bitmap bitmap = icon.getBitmap();
|
||
|
if (bitmap != null) {
|
||
|
bitmap.prepareToDraw();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (method != null) {
|
||
|
Runnable endAction = (Runnable) method.invoke(view, value);
|
||
|
if (endAction == null) {
|
||
|
return ACTION_NOOP;
|
||
|
}
|
||
|
// Special case view stub
|
||
|
if (endAction instanceof ViewStub.ViewReplaceRunnable) {
|
||
|
root.createTree();
|
||
|
// Replace child tree
|
||
|
root.findViewTreeById(mViewId).replaceView(
|
||
|
((ViewStub.ViewReplaceRunnable) endAction).view);
|
||
|
}
|
||
|
return new RunnableAction(endAction);
|
||
|
}
|
||
|
} catch (Throwable ex) {
|
||
|
throw new ActionException(ex);
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public final int mergeBehavior() {
|
||
|
// smoothScrollBy is cumulative, everything else overwites.
|
||
|
if (mMethodName.equals("smoothScrollBy")) {
|
||
|
return MERGE_APPEND;
|
||
|
} else {
|
||
|
return MERGE_REPLACE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public final String getUniqueKey() {
|
||
|
// Each type of reflection action corresponds to a setter, so each should be seen as
|
||
|
// unique from the standpoint of merging.
|
||
|
return super.getUniqueKey() + this.mMethodName + this.mType;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public final boolean prefersAsyncApply() {
|
||
|
return this.mType == URI || this.mType == ICON;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
switch (this.mType) {
|
||
|
case URI:
|
||
|
final Uri uri = (Uri) getParameterValue(null);
|
||
|
if (uri != null) visitor.accept(uri);
|
||
|
break;
|
||
|
case ICON:
|
||
|
final Icon icon = (Icon) getParameterValue(null);
|
||
|
if (icon != null) visitIconUri(icon, visitor);
|
||
|
break;
|
||
|
case INTENT:
|
||
|
final Intent intent = (Intent) getParameterValue(null);
|
||
|
if (intent != null) intent.visitUris(visitor);
|
||
|
break;
|
||
|
// TODO(b/281044385): Should we do anything about type BUNDLE?
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Class for the reflection actions. */
|
||
|
private static final class ReflectionAction extends BaseReflectionAction {
|
||
|
@UnsupportedAppUsage
|
||
|
Object mValue;
|
||
|
|
||
|
ReflectionAction(@IdRes int viewId, String methodName, int type, Object value) {
|
||
|
super(viewId, methodName, type);
|
||
|
this.mValue = value;
|
||
|
}
|
||
|
|
||
|
ReflectionAction(Parcel in) {
|
||
|
super(in);
|
||
|
// For some values that may have been null, we first check a flag to see if they were
|
||
|
// written to the parcel.
|
||
|
switch (this.mType) {
|
||
|
case BOOLEAN:
|
||
|
this.mValue = in.readBoolean();
|
||
|
break;
|
||
|
case BYTE:
|
||
|
this.mValue = in.readByte();
|
||
|
break;
|
||
|
case SHORT:
|
||
|
this.mValue = (short) in.readInt();
|
||
|
break;
|
||
|
case INT:
|
||
|
this.mValue = in.readInt();
|
||
|
break;
|
||
|
case LONG:
|
||
|
this.mValue = in.readLong();
|
||
|
break;
|
||
|
case FLOAT:
|
||
|
this.mValue = in.readFloat();
|
||
|
break;
|
||
|
case DOUBLE:
|
||
|
this.mValue = in.readDouble();
|
||
|
break;
|
||
|
case CHAR:
|
||
|
this.mValue = (char) in.readInt();
|
||
|
break;
|
||
|
case STRING:
|
||
|
this.mValue = in.readString8();
|
||
|
break;
|
||
|
case CHAR_SEQUENCE:
|
||
|
this.mValue = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
||
|
break;
|
||
|
case URI:
|
||
|
this.mValue = in.readTypedObject(Uri.CREATOR);
|
||
|
break;
|
||
|
case BITMAP:
|
||
|
this.mValue = in.readTypedObject(Bitmap.CREATOR);
|
||
|
break;
|
||
|
case BUNDLE:
|
||
|
// Because we use Parcel.allowSquashing() when writing, and that affects
|
||
|
// how the contents of Bundles are written, we need to ensure the bundle is
|
||
|
// unparceled immediately, not lazily. Setting a custom ReadWriteHelper
|
||
|
// just happens to have that effect on Bundle.readFromParcel().
|
||
|
// TODO(b/212731590): build this state tracking into Bundle
|
||
|
if (in.hasReadWriteHelper()) {
|
||
|
this.mValue = in.readBundle();
|
||
|
} else {
|
||
|
in.setReadWriteHelper(ALTERNATIVE_DEFAULT);
|
||
|
this.mValue = in.readBundle();
|
||
|
in.setReadWriteHelper(null);
|
||
|
}
|
||
|
break;
|
||
|
case INTENT:
|
||
|
this.mValue = in.readTypedObject(Intent.CREATOR);
|
||
|
break;
|
||
|
case COLOR_STATE_LIST:
|
||
|
this.mValue = in.readTypedObject(ColorStateList.CREATOR);
|
||
|
break;
|
||
|
case ICON:
|
||
|
this.mValue = in.readTypedObject(Icon.CREATOR);
|
||
|
break;
|
||
|
case BLEND_MODE:
|
||
|
this.mValue = BlendMode.fromValue(in.readInt());
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
super.writeToParcel(out, flags);
|
||
|
// For some values which are null, we record an integer flag to indicate whether
|
||
|
// we have written a valid value to the parcel.
|
||
|
switch (this.mType) {
|
||
|
case BOOLEAN:
|
||
|
out.writeBoolean((Boolean) this.mValue);
|
||
|
break;
|
||
|
case BYTE:
|
||
|
out.writeByte((Byte) this.mValue);
|
||
|
break;
|
||
|
case SHORT:
|
||
|
out.writeInt((Short) this.mValue);
|
||
|
break;
|
||
|
case INT:
|
||
|
out.writeInt((Integer) this.mValue);
|
||
|
break;
|
||
|
case LONG:
|
||
|
out.writeLong((Long) this.mValue);
|
||
|
break;
|
||
|
case FLOAT:
|
||
|
out.writeFloat((Float) this.mValue);
|
||
|
break;
|
||
|
case DOUBLE:
|
||
|
out.writeDouble((Double) this.mValue);
|
||
|
break;
|
||
|
case CHAR:
|
||
|
out.writeInt((int) ((Character) this.mValue).charValue());
|
||
|
break;
|
||
|
case STRING:
|
||
|
out.writeString8((String) this.mValue);
|
||
|
break;
|
||
|
case CHAR_SEQUENCE:
|
||
|
TextUtils.writeToParcel((CharSequence) this.mValue, out, flags);
|
||
|
break;
|
||
|
case BUNDLE:
|
||
|
out.writeBundle((Bundle) this.mValue);
|
||
|
break;
|
||
|
case BLEND_MODE:
|
||
|
out.writeInt(BlendMode.toValue((BlendMode) this.mValue));
|
||
|
break;
|
||
|
case URI:
|
||
|
case BITMAP:
|
||
|
case INTENT:
|
||
|
case COLOR_STATE_LIST:
|
||
|
case ICON:
|
||
|
out.writeTypedObject((Parcelable) this.mValue, flags);
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
@Override
|
||
|
protected Object getParameterValue(@Nullable View view) throws ActionException {
|
||
|
return this.mValue;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static final class ResourceReflectionAction extends BaseReflectionAction {
|
||
|
static final int DIMEN_RESOURCE = 1;
|
||
|
static final int COLOR_RESOURCE = 2;
|
||
|
static final int STRING_RESOURCE = 3;
|
||
|
|
||
|
private final int mResourceType;
|
||
|
private final int mResId;
|
||
|
|
||
|
ResourceReflectionAction(@IdRes int viewId, String methodName, int parameterType,
|
||
|
int resourceType, int resId) {
|
||
|
super(viewId, methodName, parameterType);
|
||
|
this.mResourceType = resourceType;
|
||
|
this.mResId = resId;
|
||
|
}
|
||
|
|
||
|
ResourceReflectionAction(Parcel in) {
|
||
|
super(in);
|
||
|
this.mResourceType = in.readInt();
|
||
|
this.mResId = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
super.writeToParcel(dest, flags);
|
||
|
dest.writeInt(this.mResourceType);
|
||
|
dest.writeInt(this.mResId);
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
@Override
|
||
|
protected Object getParameterValue(@Nullable View view) throws ActionException {
|
||
|
if (view == null) return null;
|
||
|
|
||
|
Resources resources = view.getContext().getResources();
|
||
|
try {
|
||
|
switch (this.mResourceType) {
|
||
|
case DIMEN_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.INT:
|
||
|
return mResId == 0 ? 0 : resources.getDimensionPixelSize(mResId);
|
||
|
case BaseReflectionAction.FLOAT:
|
||
|
return mResId == 0 ? 0f : resources.getDimension(mResId);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"dimen resources must be used as INT or FLOAT, "
|
||
|
+ "not " + this.mType);
|
||
|
}
|
||
|
case COLOR_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.INT:
|
||
|
return mResId == 0 ? 0 : view.getContext().getColor(mResId);
|
||
|
case BaseReflectionAction.COLOR_STATE_LIST:
|
||
|
return mResId == 0
|
||
|
? null : view.getContext().getColorStateList(mResId);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"color resources must be used as INT or COLOR_STATE_LIST,"
|
||
|
+ " not " + this.mType);
|
||
|
}
|
||
|
case STRING_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.CHAR_SEQUENCE:
|
||
|
return mResId == 0 ? null : resources.getText(mResId);
|
||
|
case BaseReflectionAction.STRING:
|
||
|
return mResId == 0 ? null : resources.getString(mResId);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"string resources must be used as STRING or CHAR_SEQUENCE,"
|
||
|
+ " not " + this.mType);
|
||
|
}
|
||
|
default:
|
||
|
throw new ActionException("unknown resource type: " + this.mResourceType);
|
||
|
}
|
||
|
} catch (ActionException ex) {
|
||
|
throw ex;
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return RESOURCE_REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static final class AttributeReflectionAction extends BaseReflectionAction {
|
||
|
static final int DIMEN_RESOURCE = 1;
|
||
|
static final int COLOR_RESOURCE = 2;
|
||
|
static final int STRING_RESOURCE = 3;
|
||
|
|
||
|
private final int mResourceType;
|
||
|
private final int mAttrId;
|
||
|
|
||
|
AttributeReflectionAction(@IdRes int viewId, String methodName, int parameterType,
|
||
|
int resourceType, int attrId) {
|
||
|
super(viewId, methodName, parameterType);
|
||
|
this.mResourceType = resourceType;
|
||
|
this.mAttrId = attrId;
|
||
|
}
|
||
|
|
||
|
AttributeReflectionAction(Parcel in) {
|
||
|
super(in);
|
||
|
this.mResourceType = in.readInt();
|
||
|
this.mAttrId = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
super.writeToParcel(dest, flags);
|
||
|
dest.writeInt(this.mResourceType);
|
||
|
dest.writeInt(this.mAttrId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected Object getParameterValue(View view) throws ActionException {
|
||
|
TypedArray typedArray = view.getContext().obtainStyledAttributes(new int[]{mAttrId});
|
||
|
try {
|
||
|
// When mAttrId == 0, we will depend on the default values below
|
||
|
if (mAttrId != 0 && typedArray.getType(0) == TypedValue.TYPE_NULL) {
|
||
|
throw new ActionException("Attribute 0x" + Integer.toHexString(this.mAttrId)
|
||
|
+ " is not defined");
|
||
|
}
|
||
|
switch (this.mResourceType) {
|
||
|
case DIMEN_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.INT:
|
||
|
return typedArray.getDimensionPixelSize(0, 0);
|
||
|
case BaseReflectionAction.FLOAT:
|
||
|
return typedArray.getDimension(0, 0);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"dimen attribute 0x" + Integer.toHexString(this.mAttrId)
|
||
|
+ " must be used as INT or FLOAT,"
|
||
|
+ " not " + this.mType);
|
||
|
}
|
||
|
case COLOR_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.INT:
|
||
|
return typedArray.getColor(0, 0);
|
||
|
case BaseReflectionAction.COLOR_STATE_LIST:
|
||
|
return typedArray.getColorStateList(0);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"color attribute 0x" + Integer.toHexString(this.mAttrId)
|
||
|
+ " must be used as INT or COLOR_STATE_LIST,"
|
||
|
+ " not " + this.mType);
|
||
|
}
|
||
|
case STRING_RESOURCE:
|
||
|
switch (this.mType) {
|
||
|
case BaseReflectionAction.CHAR_SEQUENCE:
|
||
|
return typedArray.getText(0);
|
||
|
case BaseReflectionAction.STRING:
|
||
|
return typedArray.getString(0);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"string attribute 0x" + Integer.toHexString(this.mAttrId)
|
||
|
+ " must be used as STRING or CHAR_SEQUENCE,"
|
||
|
+ " not " + this.mType);
|
||
|
}
|
||
|
default:
|
||
|
// Note: This can only be an implementation error.
|
||
|
throw new ActionException(
|
||
|
"Unknown resource type: " + this.mResourceType);
|
||
|
}
|
||
|
} catch (ActionException ex) {
|
||
|
throw ex;
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
} finally {
|
||
|
typedArray.recycle();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return ATTRIBUTE_REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
private static final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction {
|
||
|
private final float mValue;
|
||
|
@ComplexDimensionUnit
|
||
|
private final int mUnit;
|
||
|
|
||
|
ComplexUnitDimensionReflectionAction(int viewId, String methodName, int parameterType,
|
||
|
float value, @ComplexDimensionUnit int unit) {
|
||
|
super(viewId, methodName, parameterType);
|
||
|
this.mValue = value;
|
||
|
this.mUnit = unit;
|
||
|
}
|
||
|
|
||
|
ComplexUnitDimensionReflectionAction(Parcel in) {
|
||
|
super(in);
|
||
|
this.mValue = in.readFloat();
|
||
|
this.mUnit = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
super.writeToParcel(dest, flags);
|
||
|
dest.writeFloat(this.mValue);
|
||
|
dest.writeInt(this.mUnit);
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
@Override
|
||
|
protected Object getParameterValue(@Nullable View view) throws ActionException {
|
||
|
if (view == null) return null;
|
||
|
|
||
|
DisplayMetrics dm = view.getContext().getResources().getDisplayMetrics();
|
||
|
try {
|
||
|
int data = TypedValue.createComplexDimension(this.mValue, this.mUnit);
|
||
|
switch (this.mType) {
|
||
|
case ReflectionAction.INT:
|
||
|
return TypedValue.complexToDimensionPixelSize(data, dm);
|
||
|
case ReflectionAction.FLOAT:
|
||
|
return TypedValue.complexToDimension(data, dm);
|
||
|
default:
|
||
|
throw new ActionException(
|
||
|
"parameter type must be INT or FLOAT, not " + this.mType);
|
||
|
}
|
||
|
} catch (ActionException ex) {
|
||
|
throw ex;
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static final class NightModeReflectionAction extends BaseReflectionAction {
|
||
|
private final Object mLightValue;
|
||
|
private final Object mDarkValue;
|
||
|
|
||
|
NightModeReflectionAction(
|
||
|
@IdRes int viewId,
|
||
|
String methodName,
|
||
|
int type,
|
||
|
Object lightValue,
|
||
|
Object darkValue) {
|
||
|
super(viewId, methodName, type);
|
||
|
mLightValue = lightValue;
|
||
|
mDarkValue = darkValue;
|
||
|
}
|
||
|
|
||
|
NightModeReflectionAction(Parcel in) {
|
||
|
super(in);
|
||
|
switch (this.mType) {
|
||
|
case ICON:
|
||
|
mLightValue = in.readTypedObject(Icon.CREATOR);
|
||
|
mDarkValue = in.readTypedObject(Icon.CREATOR);
|
||
|
break;
|
||
|
case COLOR_STATE_LIST:
|
||
|
mLightValue = in.readTypedObject(ColorStateList.CREATOR);
|
||
|
mDarkValue = in.readTypedObject(ColorStateList.CREATOR);
|
||
|
break;
|
||
|
case INT:
|
||
|
mLightValue = in.readInt();
|
||
|
mDarkValue = in.readInt();
|
||
|
break;
|
||
|
default:
|
||
|
throw new ActionException("Unexpected night mode action type: " + this.mType);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
super.writeToParcel(out, flags);
|
||
|
switch (this.mType) {
|
||
|
case ICON:
|
||
|
case COLOR_STATE_LIST:
|
||
|
out.writeTypedObject((Parcelable) mLightValue, flags);
|
||
|
out.writeTypedObject((Parcelable) mDarkValue, flags);
|
||
|
break;
|
||
|
case INT:
|
||
|
out.writeInt((int) mLightValue);
|
||
|
out.writeInt((int) mDarkValue);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
@Override
|
||
|
protected Object getParameterValue(@Nullable View view) throws ActionException {
|
||
|
if (view == null) return null;
|
||
|
|
||
|
Configuration configuration = view.getResources().getConfiguration();
|
||
|
return configuration.isNightModeActive() ? mDarkValue : mLightValue;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return NIGHT_MODE_REFLECTION_ACTION_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
if (this.mType == ICON) {
|
||
|
visitIconUri((Icon) mDarkValue, visitor);
|
||
|
visitIconUri((Icon) mLightValue, visitor);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is only used for async execution of actions and it not parcelable.
|
||
|
*/
|
||
|
private static final class RunnableAction extends RuntimeAction {
|
||
|
private final Runnable mRunnable;
|
||
|
|
||
|
RunnableAction(Runnable r) {
|
||
|
mRunnable = r;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
mRunnable.run();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean hasStableId(View view) {
|
||
|
Object tag = view.getTag(com.android.internal.R.id.remote_views_stable_id);
|
||
|
return tag != null;
|
||
|
}
|
||
|
|
||
|
private static int getStableId(View view) {
|
||
|
Integer id = (Integer) view.getTag(com.android.internal.R.id.remote_views_stable_id);
|
||
|
return id == null ? ViewGroupActionAdd.NO_ID : id;
|
||
|
}
|
||
|
|
||
|
private static void setStableId(View view, int stableId) {
|
||
|
view.setTagInternal(com.android.internal.R.id.remote_views_stable_id, stableId);
|
||
|
}
|
||
|
|
||
|
// Returns the next recyclable child of the view group, or -1 if there are none.
|
||
|
private static int getNextRecyclableChild(ViewGroup vg) {
|
||
|
Integer tag = (Integer) vg.getTag(com.android.internal.R.id.remote_views_next_child);
|
||
|
return tag == null ? -1 : tag;
|
||
|
}
|
||
|
|
||
|
private static int getViewLayoutId(View v) {
|
||
|
return (Integer) v.getTag(R.id.widget_frame);
|
||
|
}
|
||
|
|
||
|
private static void setNextRecyclableChild(ViewGroup vg, int nextChild, int numChildren) {
|
||
|
if (nextChild < 0 || nextChild >= numChildren) {
|
||
|
vg.setTagInternal(com.android.internal.R.id.remote_views_next_child, -1);
|
||
|
} else {
|
||
|
vg.setTagInternal(com.android.internal.R.id.remote_views_next_child, nextChild);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void finalizeViewRecycling(ViewGroup root) {
|
||
|
// Remove any recyclable children that were not used. nextChild should either be -1 or point
|
||
|
// to the next recyclable child that hasn't been recycled.
|
||
|
int nextChild = getNextRecyclableChild(root);
|
||
|
if (nextChild >= 0 && nextChild < root.getChildCount()) {
|
||
|
root.removeViews(nextChild, root.getChildCount() - nextChild);
|
||
|
}
|
||
|
// Make sure on the next round, we don't try to recycle if removeAllViews is not called.
|
||
|
setNextRecyclableChild(root, -1, 0);
|
||
|
// Traverse the view tree.
|
||
|
for (int i = 0; i < root.getChildCount(); i++) {
|
||
|
View child = root.getChildAt(i);
|
||
|
if (child instanceof ViewGroup && !child.isRootNamespace()) {
|
||
|
finalizeViewRecycling((ViewGroup) child);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* ViewGroup methods that are related to adding Views.
|
||
|
*/
|
||
|
private class ViewGroupActionAdd extends Action {
|
||
|
static final int NO_ID = -1;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private RemoteViews mNestedViews;
|
||
|
private int mIndex;
|
||
|
private int mStableId;
|
||
|
|
||
|
ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews) {
|
||
|
this(viewId, nestedViews, -1 /* index */, NO_ID /* nestedViewId */);
|
||
|
}
|
||
|
|
||
|
ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews, int index) {
|
||
|
this(viewId, nestedViews, index, NO_ID /* nestedViewId */);
|
||
|
}
|
||
|
|
||
|
ViewGroupActionAdd(@IdRes int viewId, RemoteViews nestedViews, int index, int stableId) {
|
||
|
this.mViewId = viewId;
|
||
|
mNestedViews = nestedViews;
|
||
|
mIndex = index;
|
||
|
mStableId = stableId;
|
||
|
nestedViews.configureAsChild(getHierarchyRootData());
|
||
|
}
|
||
|
|
||
|
ViewGroupActionAdd(Parcel parcel, ApplicationInfo info, int depth) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mIndex = parcel.readInt();
|
||
|
mStableId = parcel.readInt();
|
||
|
mNestedViews = new RemoteViews(parcel, getHierarchyRootData(), info, depth);
|
||
|
mNestedViews.addFlags(mApplyFlags);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mIndex);
|
||
|
dest.writeInt(mStableId);
|
||
|
mNestedViews.writeToParcel(dest, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setHierarchyRootData(HierarchyRootData root) {
|
||
|
mNestedViews.configureAsChild(root);
|
||
|
}
|
||
|
|
||
|
private int findViewIndexToRecycle(ViewGroup target, RemoteViews newContent) {
|
||
|
for (int nextChild = getNextRecyclableChild(target); nextChild < target.getChildCount();
|
||
|
nextChild++) {
|
||
|
View child = target.getChildAt(nextChild);
|
||
|
if (getStableId(child) == mStableId) {
|
||
|
return nextChild;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final Context context = root.getContext();
|
||
|
final ViewGroup target = root.findViewById(mViewId);
|
||
|
|
||
|
if (target == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If removeAllViews was called, this returns the next potential recycled view.
|
||
|
// If there are no more views to recycle (or removeAllViews was not called), this
|
||
|
// will return -1.
|
||
|
final int nextChild = getNextRecyclableChild(target);
|
||
|
RemoteViews rvToApply = mNestedViews.getRemoteViewsToApply(context);
|
||
|
|
||
|
int flagsToPropagate = mApplyFlags & FLAG_MASK_TO_PROPAGATE;
|
||
|
if (flagsToPropagate != 0) rvToApply.addFlags(flagsToPropagate);
|
||
|
|
||
|
if (nextChild >= 0 && mStableId != NO_ID) {
|
||
|
// At that point, the views starting at index nextChild are the ones recyclable but
|
||
|
// not yet recycled. All views added on that round of application are placed before.
|
||
|
// Find the next view with the same stable id, or -1.
|
||
|
int recycledViewIndex = findViewIndexToRecycle(target, rvToApply);
|
||
|
if (recycledViewIndex >= 0) {
|
||
|
View child = target.getChildAt(recycledViewIndex);
|
||
|
if (rvToApply.canRecycleView(child)) {
|
||
|
if (nextChild < recycledViewIndex) {
|
||
|
target.removeViews(nextChild, recycledViewIndex - nextChild);
|
||
|
}
|
||
|
setNextRecyclableChild(target, nextChild + 1, target.getChildCount());
|
||
|
rvToApply.reapplyNestedViews(context, child, rootParent, params);
|
||
|
return;
|
||
|
}
|
||
|
// If we cannot recycle the views, we still remove all views in between to
|
||
|
// avoid weird behaviors and insert the new view in place of the old one.
|
||
|
target.removeViews(nextChild, recycledViewIndex - nextChild + 1);
|
||
|
}
|
||
|
}
|
||
|
// If we cannot recycle, insert the new view before the next recyclable child.
|
||
|
|
||
|
// Inflate nested views and add as children
|
||
|
View nestedView = rvToApply.apply(context, target, rootParent, null /* size */, params);
|
||
|
if (mStableId != NO_ID) {
|
||
|
setStableId(nestedView, mStableId);
|
||
|
}
|
||
|
target.addView(nestedView, mIndex >= 0 ? mIndex : nextChild);
|
||
|
if (nextChild >= 0) {
|
||
|
// If we are at the end, there is no reason to try to recycle anymore
|
||
|
setNextRecyclableChild(target, nextChild + 1, target.getChildCount());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
// In the async implementation, update the view tree so that subsequent calls to
|
||
|
// findViewById return the current view.
|
||
|
root.createTree();
|
||
|
ViewTree target = root.findViewTreeById(mViewId);
|
||
|
if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
|
||
|
return ACTION_NOOP;
|
||
|
}
|
||
|
final ViewGroup targetVg = (ViewGroup) target.mRoot;
|
||
|
|
||
|
// Inflate nested views and perform all the async tasks for the child remoteView.
|
||
|
final Context context = root.mRoot.getContext();
|
||
|
|
||
|
// If removeAllViews was called, this returns the next potential recycled view.
|
||
|
// If there are no more views to recycle (or removeAllViews was not called), this
|
||
|
// will return -1.
|
||
|
final int nextChild = getNextRecyclableChild(targetVg);
|
||
|
if (nextChild >= 0 && mStableId != NO_ID) {
|
||
|
RemoteViews rvToApply = mNestedViews.getRemoteViewsToApply(context);
|
||
|
final int recycledViewIndex = target.findChildIndex(nextChild,
|
||
|
view -> getStableId(view) == mStableId);
|
||
|
if (recycledViewIndex >= 0) {
|
||
|
// At that point, the views starting at index nextChild are the ones
|
||
|
// recyclable but not yet recycled. All views added on that round of
|
||
|
// application are placed before.
|
||
|
ViewTree recycled = target.mChildren.get(recycledViewIndex);
|
||
|
// We can only recycle the view if the layout id is the same.
|
||
|
if (rvToApply.canRecycleView(recycled.mRoot)) {
|
||
|
if (recycledViewIndex > nextChild) {
|
||
|
target.removeChildren(nextChild, recycledViewIndex - nextChild);
|
||
|
}
|
||
|
setNextRecyclableChild(targetVg, nextChild + 1, target.mChildren.size());
|
||
|
final AsyncApplyTask reapplyTask = rvToApply.getInternalAsyncApplyTask(
|
||
|
context,
|
||
|
targetVg, null /* listener */, params, null /* size */,
|
||
|
recycled.mRoot);
|
||
|
final ViewTree tree = reapplyTask.doInBackground();
|
||
|
if (tree == null) {
|
||
|
throw new ActionException(reapplyTask.mError);
|
||
|
}
|
||
|
return new RuntimeAction() {
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) throws ActionException {
|
||
|
reapplyTask.onPostExecute(tree);
|
||
|
if (recycledViewIndex > nextChild) {
|
||
|
targetVg.removeViews(nextChild, recycledViewIndex - nextChild);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
// If the layout id is different, still remove the children as if we recycled
|
||
|
// the view, to insert at the same place.
|
||
|
target.removeChildren(nextChild, recycledViewIndex - nextChild + 1);
|
||
|
return insertNewView(context, target, params,
|
||
|
() -> targetVg.removeViews(nextChild,
|
||
|
recycledViewIndex - nextChild + 1));
|
||
|
|
||
|
}
|
||
|
}
|
||
|
// If we cannot recycle, simply add the view at the same available slot.
|
||
|
return insertNewView(context, target, params, () -> {});
|
||
|
}
|
||
|
|
||
|
private Action insertNewView(Context context, ViewTree target,
|
||
|
ActionApplyParams params, Runnable finalizeAction) {
|
||
|
ViewGroup targetVg = (ViewGroup) target.mRoot;
|
||
|
int nextChild = getNextRecyclableChild(targetVg);
|
||
|
final AsyncApplyTask task = mNestedViews.getInternalAsyncApplyTask(context, targetVg,
|
||
|
null /* listener */, params, null /* size */, null /* result */);
|
||
|
final ViewTree tree = task.doInBackground();
|
||
|
|
||
|
if (tree == null) {
|
||
|
throw new ActionException(task.mError);
|
||
|
}
|
||
|
if (mStableId != NO_ID) {
|
||
|
setStableId(task.mResult, mStableId);
|
||
|
}
|
||
|
|
||
|
// Update the global view tree, so that next call to findViewTreeById
|
||
|
// goes through the subtree as well.
|
||
|
final int insertIndex = mIndex >= 0 ? mIndex : nextChild;
|
||
|
target.addChild(tree, insertIndex);
|
||
|
if (nextChild >= 0) {
|
||
|
setNextRecyclableChild(targetVg, nextChild + 1, target.mChildren.size());
|
||
|
}
|
||
|
|
||
|
return new RuntimeAction() {
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
task.onPostExecute(tree);
|
||
|
finalizeAction.run();
|
||
|
targetVg.addView(task.mResult, insertIndex);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int mergeBehavior() {
|
||
|
return MERGE_APPEND;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean prefersAsyncApply() {
|
||
|
return mNestedViews.prefersAsyncApply();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return VIEW_GROUP_ACTION_ADD_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
mNestedViews.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* ViewGroup methods related to removing child views.
|
||
|
*/
|
||
|
private static class ViewGroupActionRemove extends Action {
|
||
|
/**
|
||
|
* Id that indicates that all child views of the affected ViewGroup should be removed.
|
||
|
*
|
||
|
* <p>Using -2 because the default id is -1. This avoids accidentally matching that.
|
||
|
*/
|
||
|
private static final int REMOVE_ALL_VIEWS_ID = -2;
|
||
|
|
||
|
private int mViewIdToKeep;
|
||
|
|
||
|
ViewGroupActionRemove(@IdRes int viewId) {
|
||
|
this(viewId, REMOVE_ALL_VIEWS_ID);
|
||
|
}
|
||
|
|
||
|
ViewGroupActionRemove(@IdRes int viewId, @IdRes int viewIdToKeep) {
|
||
|
this.mViewId = viewId;
|
||
|
mViewIdToKeep = viewIdToKeep;
|
||
|
}
|
||
|
|
||
|
ViewGroupActionRemove(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mViewIdToKeep = parcel.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mViewIdToKeep);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final ViewGroup target = root.findViewById(mViewId);
|
||
|
|
||
|
if (target == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
|
||
|
// Remote any view without a stable id
|
||
|
for (int i = target.getChildCount() - 1; i >= 0; i--) {
|
||
|
if (!hasStableId(target.getChildAt(i))) {
|
||
|
target.removeViewAt(i);
|
||
|
}
|
||
|
}
|
||
|
// In the end, only children with a stable id (i.e. recyclable) are left.
|
||
|
setNextRecyclableChild(target, 0, target.getChildCount());
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
removeAllViewsExceptIdToKeep(target);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
// In the async implementation, update the view tree so that subsequent calls to
|
||
|
// findViewById return the current view.
|
||
|
root.createTree();
|
||
|
ViewTree target = root.findViewTreeById(mViewId);
|
||
|
|
||
|
if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
|
||
|
return ACTION_NOOP;
|
||
|
}
|
||
|
|
||
|
final ViewGroup targetVg = (ViewGroup) target.mRoot;
|
||
|
|
||
|
if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
|
||
|
target.mChildren.removeIf(childTree -> !hasStableId(childTree.mRoot));
|
||
|
setNextRecyclableChild(targetVg, 0, target.mChildren.size());
|
||
|
} else {
|
||
|
// Remove just the children which don't match the excepted view
|
||
|
target.mChildren.removeIf(childTree -> childTree.mRoot.getId() != mViewIdToKeep);
|
||
|
if (target.mChildren.isEmpty()) {
|
||
|
target.mChildren = null;
|
||
|
}
|
||
|
}
|
||
|
return new RuntimeAction() {
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
|
||
|
for (int i = targetVg.getChildCount() - 1; i >= 0; i--) {
|
||
|
if (!hasStableId(targetVg.getChildAt(i))) {
|
||
|
targetVg.removeViewAt(i);
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
removeAllViewsExceptIdToKeep(targetVg);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Iterates through the children in the given ViewGroup and removes all the views that
|
||
|
* do not have an id of {@link #mViewIdToKeep}.
|
||
|
*/
|
||
|
private void removeAllViewsExceptIdToKeep(ViewGroup viewGroup) {
|
||
|
// Otherwise, remove all the views that do not match the id to keep.
|
||
|
int index = viewGroup.getChildCount() - 1;
|
||
|
while (index >= 0) {
|
||
|
if (viewGroup.getChildAt(index).getId() != mViewIdToKeep) {
|
||
|
viewGroup.removeViewAt(index);
|
||
|
}
|
||
|
index--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return VIEW_GROUP_ACTION_REMOVE_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int mergeBehavior() {
|
||
|
return MERGE_APPEND;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Action to remove a view from its parent.
|
||
|
*/
|
||
|
private static class RemoveFromParentAction extends Action {
|
||
|
RemoveFromParentAction(@IdRes int viewId) {
|
||
|
this.mViewId = viewId;
|
||
|
}
|
||
|
|
||
|
RemoveFromParentAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
|
||
|
if (target == null || target == root) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ViewParent parent = target.getParent();
|
||
|
if (parent instanceof ViewManager) {
|
||
|
((ViewManager) parent).removeView(target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
// In the async implementation, update the view tree so that subsequent calls to
|
||
|
// findViewById return the correct view.
|
||
|
root.createTree();
|
||
|
ViewTree target = root.findViewTreeById(mViewId);
|
||
|
|
||
|
if (target == null || target == root) {
|
||
|
return ACTION_NOOP;
|
||
|
}
|
||
|
|
||
|
ViewTree parent = root.findViewTreeParentOf(target);
|
||
|
if (parent == null || !(parent.mRoot instanceof ViewManager)) {
|
||
|
return ACTION_NOOP;
|
||
|
}
|
||
|
final ViewManager parentVg = (ViewManager) parent.mRoot;
|
||
|
|
||
|
parent.mChildren.remove(target);
|
||
|
return new RuntimeAction() {
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
parentVg.removeView(target.mRoot);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return REMOVE_FROM_PARENT_ACTION_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int mergeBehavior() {
|
||
|
return MERGE_APPEND;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper action to set compound drawables on a TextView. Supports relative
|
||
|
* (s/t/e/b) or cardinal (l/t/r/b) arrangement.
|
||
|
*/
|
||
|
private static class TextViewDrawableAction extends Action {
|
||
|
boolean mIsRelative = false;
|
||
|
boolean mUseIcons = false;
|
||
|
int mD1, mD2, mD3, mD4;
|
||
|
Icon mI1, mI2, mI3, mI4;
|
||
|
|
||
|
boolean mDrawablesLoaded = false;
|
||
|
Drawable mId1, mId2, mId3, mId4;
|
||
|
|
||
|
public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, @DrawableRes int d1,
|
||
|
@DrawableRes int d2, @DrawableRes int d3, @DrawableRes int d4) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mIsRelative = isRelative;
|
||
|
this.mUseIcons = false;
|
||
|
this.mD1 = d1;
|
||
|
this.mD2 = d2;
|
||
|
this.mD3 = d3;
|
||
|
this.mD4 = d4;
|
||
|
}
|
||
|
|
||
|
public TextViewDrawableAction(@IdRes int viewId, boolean isRelative,
|
||
|
Icon i1, Icon i2, Icon i3, Icon i4) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mIsRelative = isRelative;
|
||
|
this.mUseIcons = true;
|
||
|
this.mI1 = i1;
|
||
|
this.mI2 = i2;
|
||
|
this.mI3 = i3;
|
||
|
this.mI4 = i4;
|
||
|
}
|
||
|
|
||
|
public TextViewDrawableAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mIsRelative = (parcel.readInt() != 0);
|
||
|
mUseIcons = (parcel.readInt() != 0);
|
||
|
if (mUseIcons) {
|
||
|
mI1 = parcel.readTypedObject(Icon.CREATOR);
|
||
|
mI2 = parcel.readTypedObject(Icon.CREATOR);
|
||
|
mI3 = parcel.readTypedObject(Icon.CREATOR);
|
||
|
mI4 = parcel.readTypedObject(Icon.CREATOR);
|
||
|
} else {
|
||
|
mD1 = parcel.readInt();
|
||
|
mD2 = parcel.readInt();
|
||
|
mD3 = parcel.readInt();
|
||
|
mD4 = parcel.readInt();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mIsRelative ? 1 : 0);
|
||
|
dest.writeInt(mUseIcons ? 1 : 0);
|
||
|
if (mUseIcons) {
|
||
|
dest.writeTypedObject(mI1, 0);
|
||
|
dest.writeTypedObject(mI2, 0);
|
||
|
dest.writeTypedObject(mI3, 0);
|
||
|
dest.writeTypedObject(mI4, 0);
|
||
|
} else {
|
||
|
dest.writeInt(mD1);
|
||
|
dest.writeInt(mD2);
|
||
|
dest.writeInt(mD3);
|
||
|
dest.writeInt(mD4);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final TextView target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
if (mDrawablesLoaded) {
|
||
|
if (mIsRelative) {
|
||
|
target.setCompoundDrawablesRelativeWithIntrinsicBounds(mId1, mId2, mId3, mId4);
|
||
|
} else {
|
||
|
target.setCompoundDrawablesWithIntrinsicBounds(mId1, mId2, mId3, mId4);
|
||
|
}
|
||
|
} else if (mUseIcons) {
|
||
|
final Context ctx = target.getContext();
|
||
|
final Drawable id1 = mI1 == null ? null : mI1.loadDrawable(ctx);
|
||
|
final Drawable id2 = mI2 == null ? null : mI2.loadDrawable(ctx);
|
||
|
final Drawable id3 = mI3 == null ? null : mI3.loadDrawable(ctx);
|
||
|
final Drawable id4 = mI4 == null ? null : mI4.loadDrawable(ctx);
|
||
|
if (mIsRelative) {
|
||
|
target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
|
||
|
} else {
|
||
|
target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
|
||
|
}
|
||
|
} else {
|
||
|
if (mIsRelative) {
|
||
|
target.setCompoundDrawablesRelativeWithIntrinsicBounds(mD1, mD2, mD3, mD4);
|
||
|
} else {
|
||
|
target.setCompoundDrawablesWithIntrinsicBounds(mD1, mD2, mD3, mD4);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
final TextView target = root.findViewById(mViewId);
|
||
|
if (target == null) return ACTION_NOOP;
|
||
|
|
||
|
TextViewDrawableAction copy = mUseIcons
|
||
|
? new TextViewDrawableAction(mViewId, mIsRelative, mI1, mI2, mI3, mI4)
|
||
|
: new TextViewDrawableAction(mViewId, mIsRelative, mD1, mD2, mD3, mD4);
|
||
|
|
||
|
// Load the drawables on the background thread.
|
||
|
copy.mDrawablesLoaded = true;
|
||
|
final Context ctx = target.getContext();
|
||
|
|
||
|
if (mUseIcons) {
|
||
|
copy.mId1 = mI1 == null ? null : mI1.loadDrawable(ctx);
|
||
|
copy.mId2 = mI2 == null ? null : mI2.loadDrawable(ctx);
|
||
|
copy.mId3 = mI3 == null ? null : mI3.loadDrawable(ctx);
|
||
|
copy.mId4 = mI4 == null ? null : mI4.loadDrawable(ctx);
|
||
|
} else {
|
||
|
copy.mId1 = mD1 == 0 ? null : ctx.getDrawable(mD1);
|
||
|
copy.mId2 = mD2 == 0 ? null : ctx.getDrawable(mD2);
|
||
|
copy.mId3 = mD3 == 0 ? null : ctx.getDrawable(mD3);
|
||
|
copy.mId4 = mD4 == 0 ? null : ctx.getDrawable(mD4);
|
||
|
}
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean prefersAsyncApply() {
|
||
|
return mUseIcons;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return TEXT_VIEW_DRAWABLE_ACTION_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
if (mUseIcons) {
|
||
|
visitIconUri(mI1, visitor);
|
||
|
visitIconUri(mI2, visitor);
|
||
|
visitIconUri(mI3, visitor);
|
||
|
visitIconUri(mI4, visitor);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper action to set text size on a TextView in any supported units.
|
||
|
*/
|
||
|
private static class TextViewSizeAction extends Action {
|
||
|
int mUnits;
|
||
|
float mSize;
|
||
|
|
||
|
TextViewSizeAction(@IdRes int viewId, @ComplexDimensionUnit int units, float size) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mUnits = units;
|
||
|
this.mSize = size;
|
||
|
}
|
||
|
|
||
|
TextViewSizeAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mUnits = parcel.readInt();
|
||
|
mSize = parcel.readFloat();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mUnits);
|
||
|
dest.writeFloat(mSize);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final TextView target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
target.setTextSize(mUnits, mSize);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return TEXT_VIEW_SIZE_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper action to set padding on a View.
|
||
|
*/
|
||
|
private static class ViewPaddingAction extends Action {
|
||
|
@Px int mLeft, mTop, mRight, mBottom;
|
||
|
|
||
|
public ViewPaddingAction(@IdRes int viewId, @Px int left, @Px int top,
|
||
|
@Px int right, @Px int bottom) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mLeft = left;
|
||
|
this.mTop = top;
|
||
|
this.mRight = right;
|
||
|
this.mBottom = bottom;
|
||
|
}
|
||
|
|
||
|
public ViewPaddingAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mLeft = parcel.readInt();
|
||
|
mTop = parcel.readInt();
|
||
|
mRight = parcel.readInt();
|
||
|
mBottom = parcel.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mLeft);
|
||
|
dest.writeInt(mTop);
|
||
|
dest.writeInt(mRight);
|
||
|
dest.writeInt(mBottom);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
target.setPadding(mLeft, mTop, mRight, mBottom);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return VIEW_PADDING_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper action to set layout params on a View.
|
||
|
*/
|
||
|
private static class LayoutParamAction extends Action {
|
||
|
static final int LAYOUT_MARGIN_LEFT = MARGIN_LEFT;
|
||
|
static final int LAYOUT_MARGIN_TOP = MARGIN_TOP;
|
||
|
static final int LAYOUT_MARGIN_RIGHT = MARGIN_RIGHT;
|
||
|
static final int LAYOUT_MARGIN_BOTTOM = MARGIN_BOTTOM;
|
||
|
static final int LAYOUT_MARGIN_START = MARGIN_START;
|
||
|
static final int LAYOUT_MARGIN_END = MARGIN_END;
|
||
|
static final int LAYOUT_WIDTH = 8;
|
||
|
static final int LAYOUT_HEIGHT = 9;
|
||
|
|
||
|
final int mProperty;
|
||
|
final int mValueType;
|
||
|
final int mValue;
|
||
|
|
||
|
/**
|
||
|
* @param viewId ID of the view alter
|
||
|
* @param property which layout parameter to alter
|
||
|
* @param value new value of the layout parameter
|
||
|
* @param units the units of the given value
|
||
|
*/
|
||
|
LayoutParamAction(@IdRes int viewId, int property, float value,
|
||
|
@ComplexDimensionUnit int units) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mProperty = property;
|
||
|
this.mValueType = VALUE_TYPE_COMPLEX_UNIT;
|
||
|
this.mValue = TypedValue.createComplexDimension(value, units);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param viewId ID of the view alter
|
||
|
* @param property which layout parameter to alter
|
||
|
* @param value value to set.
|
||
|
* @param valueType must be one of {@link #VALUE_TYPE_COMPLEX_UNIT},
|
||
|
* {@link #VALUE_TYPE_RESOURCE}, {@link #VALUE_TYPE_ATTRIBUTE} or
|
||
|
* {@link #VALUE_TYPE_RAW}.
|
||
|
*/
|
||
|
LayoutParamAction(@IdRes int viewId, int property, int value, @ValueType int valueType) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mProperty = property;
|
||
|
this.mValueType = valueType;
|
||
|
this.mValue = value;
|
||
|
}
|
||
|
|
||
|
public LayoutParamAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mProperty = parcel.readInt();
|
||
|
mValueType = parcel.readInt();
|
||
|
mValue = parcel.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mProperty);
|
||
|
dest.writeInt(mValueType);
|
||
|
dest.writeInt(mValue);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) {
|
||
|
return;
|
||
|
}
|
||
|
ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
|
||
|
if (layoutParams == null) {
|
||
|
return;
|
||
|
}
|
||
|
switch (mProperty) {
|
||
|
case LAYOUT_MARGIN_LEFT:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).leftMargin = getPixelOffset(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_MARGIN_TOP:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).topMargin = getPixelOffset(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_MARGIN_RIGHT:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).rightMargin = getPixelOffset(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_MARGIN_BOTTOM:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).bottomMargin = getPixelOffset(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_MARGIN_START:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).setMarginStart(getPixelOffset(target));
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_MARGIN_END:
|
||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||
|
((MarginLayoutParams) layoutParams).setMarginEnd(getPixelOffset(target));
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
}
|
||
|
break;
|
||
|
case LAYOUT_WIDTH:
|
||
|
layoutParams.width = getPixelSize(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
break;
|
||
|
case LAYOUT_HEIGHT:
|
||
|
layoutParams.height = getPixelSize(target);
|
||
|
target.setLayoutParams(layoutParams);
|
||
|
break;
|
||
|
default:
|
||
|
throw new IllegalArgumentException("Unknown property " + mProperty);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int getPixelOffset(View target) {
|
||
|
try {
|
||
|
switch (mValueType) {
|
||
|
case VALUE_TYPE_ATTRIBUTE:
|
||
|
TypedArray typedArray = target.getContext().obtainStyledAttributes(
|
||
|
new int[]{this.mValue});
|
||
|
try {
|
||
|
return typedArray.getDimensionPixelOffset(0, 0);
|
||
|
} finally {
|
||
|
typedArray.recycle();
|
||
|
}
|
||
|
case VALUE_TYPE_RESOURCE:
|
||
|
if (mValue == 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
return target.getResources().getDimensionPixelOffset(mValue);
|
||
|
case VALUE_TYPE_COMPLEX_UNIT:
|
||
|
return TypedValue.complexToDimensionPixelOffset(mValue,
|
||
|
target.getResources().getDisplayMetrics());
|
||
|
default:
|
||
|
return mValue;
|
||
|
}
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int getPixelSize(View target) {
|
||
|
try {
|
||
|
switch (mValueType) {
|
||
|
case VALUE_TYPE_ATTRIBUTE:
|
||
|
TypedArray typedArray = target.getContext().obtainStyledAttributes(
|
||
|
new int[]{this.mValue});
|
||
|
try {
|
||
|
return typedArray.getDimensionPixelSize(0, 0);
|
||
|
} finally {
|
||
|
typedArray.recycle();
|
||
|
}
|
||
|
case VALUE_TYPE_RESOURCE:
|
||
|
if (mValue == 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
return target.getResources().getDimensionPixelSize(mValue);
|
||
|
case VALUE_TYPE_COMPLEX_UNIT:
|
||
|
return TypedValue.complexToDimensionPixelSize(mValue,
|
||
|
target.getResources().getDisplayMetrics());
|
||
|
default:
|
||
|
return mValue;
|
||
|
}
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return LAYOUT_PARAM_ACTION_TAG;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getUniqueKey() {
|
||
|
return super.getUniqueKey() + mProperty;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper action to add a view tag with RemoteInputs.
|
||
|
*/
|
||
|
private static class SetRemoteInputsAction extends Action {
|
||
|
final Parcelable[] mRemoteInputs;
|
||
|
|
||
|
public SetRemoteInputsAction(@IdRes int viewId, RemoteInput[] remoteInputs) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mRemoteInputs = remoteInputs;
|
||
|
}
|
||
|
|
||
|
public SetRemoteInputsAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mRemoteInputs = parcel.createTypedArray(RemoteInput.CREATOR);
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeTypedArray(mRemoteInputs, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
target.setTagInternal(R.id.remote_input_tag, mRemoteInputs);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_REMOTE_INPUTS_ACTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetIntTagAction extends Action {
|
||
|
@IdRes private final int mViewId;
|
||
|
@IdRes private final int mKey;
|
||
|
private final int mTag;
|
||
|
|
||
|
SetIntTagAction(@IdRes int viewId, @IdRes int key, int tag) {
|
||
|
mViewId = viewId;
|
||
|
mKey = key;
|
||
|
mTag = tag;
|
||
|
}
|
||
|
|
||
|
SetIntTagAction(Parcel parcel) {
|
||
|
mViewId = parcel.readInt();
|
||
|
mKey = parcel.readInt();
|
||
|
mTag = parcel.readInt();
|
||
|
}
|
||
|
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mKey);
|
||
|
dest.writeInt(mTag);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
target.setTagInternal(mKey, mTag);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_INT_TAG_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetCompoundButtonCheckedAction extends Action {
|
||
|
private final boolean mChecked;
|
||
|
|
||
|
SetCompoundButtonCheckedAction(@IdRes int viewId, boolean checked) {
|
||
|
this.mViewId = viewId;
|
||
|
mChecked = checked;
|
||
|
}
|
||
|
|
||
|
SetCompoundButtonCheckedAction(Parcel in) {
|
||
|
mViewId = in.readInt();
|
||
|
mChecked = in.readBoolean();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeBoolean(mChecked);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
if (!(target instanceof CompoundButton)) {
|
||
|
Log.w(LOG_TAG, "Cannot set checked to view "
|
||
|
+ mViewId + " because it is not a CompoundButton");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CompoundButton button = (CompoundButton) target;
|
||
|
Object tag = button.getTag(R.id.remote_checked_change_listener_tag);
|
||
|
// Temporarily unset the checked change listener so calling setChecked doesn't launch
|
||
|
// the intent.
|
||
|
if (tag instanceof OnCheckedChangeListener) {
|
||
|
button.setOnCheckedChangeListener(null);
|
||
|
button.setChecked(mChecked);
|
||
|
button.setOnCheckedChangeListener((OnCheckedChangeListener) tag);
|
||
|
} else {
|
||
|
button.setChecked(mChecked);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_COMPOUND_BUTTON_CHECKED_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetRadioGroupCheckedAction extends Action {
|
||
|
@IdRes private final int mCheckedId;
|
||
|
|
||
|
SetRadioGroupCheckedAction(@IdRes int viewId, @IdRes int checkedId) {
|
||
|
this.mViewId = viewId;
|
||
|
mCheckedId = checkedId;
|
||
|
}
|
||
|
|
||
|
SetRadioGroupCheckedAction(Parcel in) {
|
||
|
mViewId = in.readInt();
|
||
|
mCheckedId = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mCheckedId);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
if (!(target instanceof RadioGroup)) {
|
||
|
Log.w(LOG_TAG, "Cannot check " + mViewId + " because it's not a RadioGroup");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RadioGroup group = (RadioGroup) target;
|
||
|
|
||
|
// Temporarily unset all the checked change listeners while we check the group.
|
||
|
for (int i = 0; i < group.getChildCount(); i++) {
|
||
|
View child = group.getChildAt(i);
|
||
|
if (!(child instanceof CompoundButton)) continue;
|
||
|
|
||
|
Object tag = child.getTag(R.id.remote_checked_change_listener_tag);
|
||
|
if (!(tag instanceof OnCheckedChangeListener)) continue;
|
||
|
|
||
|
// Clear the checked change listener, we'll restore it after the check.
|
||
|
((CompoundButton) child).setOnCheckedChangeListener(null);
|
||
|
}
|
||
|
|
||
|
group.check(mCheckedId);
|
||
|
|
||
|
// Loop through the children again and restore the checked change listeners.
|
||
|
for (int i = 0; i < group.getChildCount(); i++) {
|
||
|
View child = group.getChildAt(i);
|
||
|
if (!(child instanceof CompoundButton)) continue;
|
||
|
|
||
|
Object tag = child.getTag(R.id.remote_checked_change_listener_tag);
|
||
|
if (!(tag instanceof OnCheckedChangeListener)) continue;
|
||
|
|
||
|
((CompoundButton) child).setOnCheckedChangeListener((OnCheckedChangeListener) tag);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_RADIO_GROUP_CHECKED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class SetViewOutlinePreferredRadiusAction extends Action {
|
||
|
@ValueType
|
||
|
private final int mValueType;
|
||
|
private final int mValue;
|
||
|
|
||
|
SetViewOutlinePreferredRadiusAction(@IdRes int viewId, int value,
|
||
|
@ValueType int valueType) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mValueType = valueType;
|
||
|
this.mValue = value;
|
||
|
}
|
||
|
|
||
|
SetViewOutlinePreferredRadiusAction(
|
||
|
@IdRes int viewId, float radius, @ComplexDimensionUnit int units) {
|
||
|
this.mViewId = viewId;
|
||
|
this.mValueType = VALUE_TYPE_COMPLEX_UNIT;
|
||
|
this.mValue = TypedValue.createComplexDimension(radius, units);
|
||
|
|
||
|
}
|
||
|
|
||
|
SetViewOutlinePreferredRadiusAction(Parcel in) {
|
||
|
mViewId = in.readInt();
|
||
|
mValueType = in.readInt();
|
||
|
mValue = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mValueType);
|
||
|
dest.writeInt(mValue);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
final View target = root.findViewById(mViewId);
|
||
|
if (target == null) return;
|
||
|
|
||
|
try {
|
||
|
float radius;
|
||
|
switch (mValueType) {
|
||
|
case VALUE_TYPE_ATTRIBUTE:
|
||
|
TypedArray typedArray = target.getContext().obtainStyledAttributes(
|
||
|
new int[]{mValue});
|
||
|
try {
|
||
|
radius = typedArray.getDimension(0, 0);
|
||
|
} finally {
|
||
|
typedArray.recycle();
|
||
|
}
|
||
|
break;
|
||
|
case VALUE_TYPE_RESOURCE:
|
||
|
radius = mValue == 0 ? 0 : target.getResources().getDimension(mValue);
|
||
|
break;
|
||
|
case VALUE_TYPE_COMPLEX_UNIT:
|
||
|
radius = TypedValue.complexToDimension(mValue,
|
||
|
target.getResources().getDisplayMetrics());
|
||
|
break;
|
||
|
default:
|
||
|
radius = mValue;
|
||
|
}
|
||
|
target.setOutlineProvider(new RemoteViewOutlineProvider(radius));
|
||
|
} catch (Throwable t) {
|
||
|
throw new ActionException(t);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_VIEW_OUTLINE_RADIUS_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* OutlineProvider for a view with a radius set by
|
||
|
* {@link #setViewOutlinePreferredRadius(int, float, int)}.
|
||
|
*/
|
||
|
public static final class RemoteViewOutlineProvider extends ViewOutlineProvider {
|
||
|
private final float mRadius;
|
||
|
|
||
|
public RemoteViewOutlineProvider(float radius) {
|
||
|
mRadius = radius;
|
||
|
}
|
||
|
|
||
|
/** Returns the corner radius used when providing the view outline. */
|
||
|
public float getRadius() {
|
||
|
return mRadius;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void getOutline(@NonNull View view, @NonNull Outline outline) {
|
||
|
outline.setRoundRect(
|
||
|
0 /*left*/,
|
||
|
0 /* top */,
|
||
|
view.getWidth() /* right */,
|
||
|
view.getHeight() /* bottom */,
|
||
|
mRadius);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class SetDrawInstructionAction extends Action {
|
||
|
|
||
|
@Nullable
|
||
|
private final DrawInstructions mInstructions;
|
||
|
|
||
|
SetDrawInstructionAction(@NonNull final DrawInstructions instructions) {
|
||
|
mInstructions = instructions;
|
||
|
}
|
||
|
|
||
|
SetDrawInstructionAction(@NonNull final Parcel in) {
|
||
|
if (drawDataParcel()) {
|
||
|
mInstructions = DrawInstructions.readFromParcel(in);
|
||
|
} else {
|
||
|
mInstructions = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
if (drawDataParcel()) {
|
||
|
DrawInstructions.writeToParcel(mInstructions, dest, flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
|
||
|
throws ActionException {
|
||
|
if (drawDataParcel() && mInstructions != null
|
||
|
&& root instanceof RemoteComposePlayer player) {
|
||
|
final List<byte[]> bytes = mInstructions.mInstructions;
|
||
|
if (bytes.isEmpty()) {
|
||
|
return;
|
||
|
}
|
||
|
try (ByteArrayInputStream is = new ByteArrayInputStream(bytes.get(0))) {
|
||
|
player.setDocument(new RemoteComposeDocument(is));
|
||
|
player.addClickListener((viewId, metadata) -> {
|
||
|
mActions.forEach(action -> {
|
||
|
if (viewId == action.mViewId
|
||
|
&& action instanceof SetOnClickResponse setOnClickResponse) {
|
||
|
setOnClickResponse.mResponse.handleViewInteraction(
|
||
|
player, params.handler);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
} catch (IOException e) {
|
||
|
Log.e(LOG_TAG, "Failed to render draw instructions", e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getActionTag() {
|
||
|
return SET_DRAW_INSTRUCTION_TAG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RemoteViews object that will display the views contained
|
||
|
* in the specified layout file.
|
||
|
*
|
||
|
* @param packageName Name of the package that contains the layout resource
|
||
|
* @param layoutId The id of the layout resource
|
||
|
*/
|
||
|
public RemoteViews(String packageName, int layoutId) {
|
||
|
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RemoteViews object that will display the views contained
|
||
|
* in the specified layout file and change the id of the root view to the specified one.
|
||
|
*
|
||
|
* @param packageName Name of the package that contains the layout resource
|
||
|
* @param layoutId The id of the layout resource
|
||
|
*/
|
||
|
public RemoteViews(@NonNull String packageName, @LayoutRes int layoutId, @IdRes int viewId) {
|
||
|
this(packageName, layoutId);
|
||
|
this.mViewId = viewId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RemoteViews object that will display the views contained
|
||
|
* in the specified layout file.
|
||
|
*
|
||
|
* @param application The application whose content is shown by the views.
|
||
|
* @param layoutId The id of the layout resource.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
protected RemoteViews(ApplicationInfo application, @LayoutRes int layoutId) {
|
||
|
mApplication = application;
|
||
|
mLayoutId = layoutId;
|
||
|
mApplicationInfoCache.put(application);
|
||
|
}
|
||
|
|
||
|
private boolean hasMultipleLayouts() {
|
||
|
return hasLandscapeAndPortraitLayouts() || hasSizedRemoteViews();
|
||
|
}
|
||
|
|
||
|
private boolean hasLandscapeAndPortraitLayouts() {
|
||
|
return (mLandscape != null) && (mPortrait != null);
|
||
|
}
|
||
|
|
||
|
private boolean hasSizedRemoteViews() {
|
||
|
return mSizedRemoteViews != null;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private SizeF getIdealSize() {
|
||
|
return mIdealSize;
|
||
|
}
|
||
|
|
||
|
private void setIdealSize(@Nullable SizeF size) {
|
||
|
mIdealSize = size;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Finds the smallest view in {@code mSizedRemoteViews}.
|
||
|
* This method must not be called if {@code mSizedRemoteViews} is null.
|
||
|
*/
|
||
|
private RemoteViews findSmallestRemoteView() {
|
||
|
return mSizedRemoteViews.get(mSizedRemoteViews.size() - 1);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RemoteViews object that will inflate as the specified
|
||
|
* landspace or portrait RemoteViews, depending on the current configuration.
|
||
|
*
|
||
|
* @param landscape The RemoteViews to inflate in landscape configuration
|
||
|
* @param portrait The RemoteViews to inflate in portrait configuration
|
||
|
* @throws IllegalArgumentException if either landscape or portrait are null or if they are
|
||
|
* not from the same application
|
||
|
*/
|
||
|
public RemoteViews(RemoteViews landscape, RemoteViews portrait) {
|
||
|
if (landscape == null || portrait == null) {
|
||
|
throw new IllegalArgumentException("Both RemoteViews must be non-null");
|
||
|
}
|
||
|
if (!landscape.hasSameAppInfo(portrait.mApplication)) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"Both RemoteViews must share the same package and user");
|
||
|
}
|
||
|
mApplication = portrait.mApplication;
|
||
|
mLayoutId = portrait.mLayoutId;
|
||
|
mViewId = portrait.mViewId;
|
||
|
mLightBackgroundLayoutId = portrait.mLightBackgroundLayoutId;
|
||
|
|
||
|
mLandscape = landscape;
|
||
|
mPortrait = portrait;
|
||
|
|
||
|
mClassCookies = (portrait.mClassCookies != null)
|
||
|
? portrait.mClassCookies : landscape.mClassCookies;
|
||
|
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new RemoteViews object that will inflate the layout with the closest size
|
||
|
* specification.
|
||
|
*
|
||
|
* The default remote views in that case is always the one with the smallest area.
|
||
|
*
|
||
|
* If the {@link RemoteViews} host provides the size of the view, the layout with the largest
|
||
|
* area that fits entirely in the provided size will be used (i.e. the width and height of
|
||
|
* the layout must be less than the size of the view, with a 1dp margin to account for
|
||
|
* rounding). If no layout fits in the view, the layout with the smallest area will be used.
|
||
|
*
|
||
|
* @param remoteViews Mapping of size to layout.
|
||
|
* @throws IllegalArgumentException if the map is empty, there are more than
|
||
|
* MAX_INIT_VIEW_COUNT layouts or the remote views are not all from the same application.
|
||
|
*/
|
||
|
public RemoteViews(@NonNull Map<SizeF, RemoteViews> remoteViews) {
|
||
|
if (remoteViews.isEmpty()) {
|
||
|
throw new IllegalArgumentException("The set of RemoteViews cannot be empty");
|
||
|
}
|
||
|
if (remoteViews.size() > MAX_INIT_VIEW_COUNT) {
|
||
|
throw new IllegalArgumentException("Too many RemoteViews in constructor");
|
||
|
}
|
||
|
if (remoteViews.size() == 1) {
|
||
|
// If the map only contains a single mapping, treat this as if that RemoteViews was
|
||
|
// passed as the top-level RemoteViews.
|
||
|
RemoteViews single = remoteViews.values().iterator().next();
|
||
|
initializeFrom(single, /* hierarchyRoot= */ single);
|
||
|
return;
|
||
|
}
|
||
|
mClassCookies = initializeSizedRemoteViews(
|
||
|
remoteViews.entrySet().stream().map(
|
||
|
entry -> {
|
||
|
entry.getValue().setIdealSize(entry.getKey());
|
||
|
return entry.getValue();
|
||
|
}
|
||
|
).iterator()
|
||
|
);
|
||
|
|
||
|
RemoteViews smallestView = findSmallestRemoteView();
|
||
|
mApplication = smallestView.mApplication;
|
||
|
mLayoutId = smallestView.mLayoutId;
|
||
|
mViewId = smallestView.mViewId;
|
||
|
mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId;
|
||
|
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
|
||
|
// Initialize mSizedRemoteViews and return the class cookies.
|
||
|
private Map<Class, Object> initializeSizedRemoteViews(Iterator<RemoteViews> remoteViews) {
|
||
|
List<RemoteViews> sizedRemoteViews = new ArrayList<>();
|
||
|
Map<Class, Object> classCookies = null;
|
||
|
float viewArea = Float.MAX_VALUE;
|
||
|
RemoteViews smallestView = null;
|
||
|
while (remoteViews.hasNext()) {
|
||
|
RemoteViews view = remoteViews.next();
|
||
|
SizeF size = view.getIdealSize();
|
||
|
if (size == null) {
|
||
|
throw new IllegalStateException("Expected RemoteViews to have ideal size");
|
||
|
}
|
||
|
float newViewArea = size.getWidth() * size.getHeight();
|
||
|
if (smallestView != null && !view.hasSameAppInfo(smallestView.mApplication)) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"All RemoteViews must share the same package and user");
|
||
|
}
|
||
|
if (smallestView == null || newViewArea < viewArea) {
|
||
|
if (smallestView != null) {
|
||
|
sizedRemoteViews.add(smallestView);
|
||
|
}
|
||
|
viewArea = newViewArea;
|
||
|
smallestView = view;
|
||
|
} else {
|
||
|
sizedRemoteViews.add(view);
|
||
|
}
|
||
|
view.setIdealSize(size);
|
||
|
if (classCookies == null) {
|
||
|
classCookies = view.mClassCookies;
|
||
|
}
|
||
|
}
|
||
|
sizedRemoteViews.add(smallestView);
|
||
|
mSizedRemoteViews = sizedRemoteViews;
|
||
|
return classCookies;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a copy of another RemoteViews.
|
||
|
*/
|
||
|
public RemoteViews(RemoteViews src) {
|
||
|
initializeFrom(src, /* hierarchyRoot= */ null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* No-arg constructor for use with {@link #initializeFrom(RemoteViews, RemoteViews)}. A
|
||
|
* constructor taking two RemoteViews parameters would clash with the landscape/portrait
|
||
|
* constructor.
|
||
|
*/
|
||
|
private RemoteViews() {}
|
||
|
|
||
|
private static RemoteViews createInitializedFrom(@NonNull RemoteViews src,
|
||
|
@Nullable RemoteViews hierarchyRoot) {
|
||
|
RemoteViews child = new RemoteViews();
|
||
|
child.initializeFrom(src, hierarchyRoot);
|
||
|
return child;
|
||
|
}
|
||
|
|
||
|
private void initializeFrom(@NonNull RemoteViews src, @Nullable RemoteViews hierarchyRoot) {
|
||
|
if (hierarchyRoot == null) {
|
||
|
mBitmapCache = src.mBitmapCache;
|
||
|
// We need to create a new instance because we don't reconstruct collection cache
|
||
|
mCollectionCache = new RemoteCollectionCache(src.mCollectionCache);
|
||
|
mApplicationInfoCache = src.mApplicationInfoCache;
|
||
|
} else {
|
||
|
mBitmapCache = hierarchyRoot.mBitmapCache;
|
||
|
mCollectionCache = hierarchyRoot.mCollectionCache;
|
||
|
mApplicationInfoCache = hierarchyRoot.mApplicationInfoCache;
|
||
|
}
|
||
|
if (hierarchyRoot == null || src.mIsRoot) {
|
||
|
// If there's no provided root, or if src was itself a root, then this RemoteViews is
|
||
|
// the root of the new hierarchy.
|
||
|
mIsRoot = true;
|
||
|
hierarchyRoot = this;
|
||
|
} else {
|
||
|
// Otherwise, we're a descendant in the hierarchy.
|
||
|
mIsRoot = false;
|
||
|
}
|
||
|
mApplication = src.mApplication;
|
||
|
mLayoutId = src.mLayoutId;
|
||
|
mLightBackgroundLayoutId = src.mLightBackgroundLayoutId;
|
||
|
mApplyFlags = src.mApplyFlags;
|
||
|
mClassCookies = src.mClassCookies;
|
||
|
mIdealSize = src.mIdealSize;
|
||
|
mProviderInstanceId = src.mProviderInstanceId;
|
||
|
mHasDrawInstructions = src.mHasDrawInstructions;
|
||
|
|
||
|
if (src.hasLandscapeAndPortraitLayouts()) {
|
||
|
mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot);
|
||
|
mPortrait = createInitializedFrom(src.mPortrait, hierarchyRoot);
|
||
|
}
|
||
|
|
||
|
if (src.hasSizedRemoteViews()) {
|
||
|
mSizedRemoteViews = new ArrayList<>(src.mSizedRemoteViews.size());
|
||
|
for (RemoteViews srcView : src.mSizedRemoteViews) {
|
||
|
mSizedRemoteViews.add(createInitializedFrom(srcView, hierarchyRoot));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (src.mActions != null) {
|
||
|
Parcel p = Parcel.obtain();
|
||
|
p.putClassCookies(mClassCookies);
|
||
|
src.writeActionsToParcel(p, /* flags= */ 0);
|
||
|
p.setDataPosition(0);
|
||
|
// Since src is already in memory, we do not care about stack overflow as it has
|
||
|
// already been read once.
|
||
|
readActionsFromParcel(p, 0);
|
||
|
p.recycle();
|
||
|
}
|
||
|
|
||
|
// Now that everything is initialized and duplicated, create new caches for this
|
||
|
// RemoteViews and recursively set up all descendants.
|
||
|
if (mIsRoot) {
|
||
|
reconstructCaches();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reads a RemoteViews object from a parcel.
|
||
|
*
|
||
|
* @param parcel the parcel object
|
||
|
*/
|
||
|
public RemoteViews(Parcel parcel) {
|
||
|
this(parcel, /* rootData= */ null, /* info= */ null, /* depth= */ 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Instantiates a RemoteViews object using {@link DrawInstructions}, which serves as an
|
||
|
* alternative to XML layout. {@link DrawInstructions} objects contains the instructions which
|
||
|
* can be interpreted and rendered accordingly in the host process.
|
||
|
*
|
||
|
* @param drawInstructions The {@link DrawInstructions} object
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public RemoteViews(@NonNull final DrawInstructions drawInstructions) {
|
||
|
Objects.requireNonNull(drawInstructions);
|
||
|
mHasDrawInstructions = true;
|
||
|
addAction(new SetDrawInstructionAction(drawInstructions));
|
||
|
}
|
||
|
|
||
|
private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData,
|
||
|
@Nullable ApplicationInfo info, int depth) {
|
||
|
if (depth > MAX_NESTED_VIEWS
|
||
|
&& (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) {
|
||
|
throw new IllegalArgumentException("Too many nested views.");
|
||
|
}
|
||
|
depth++;
|
||
|
|
||
|
int mode = parcel.readInt();
|
||
|
|
||
|
if (rootData == null) {
|
||
|
// We only store a bitmap cache in the root of the RemoteViews.
|
||
|
mBitmapCache = new BitmapCache(parcel);
|
||
|
// Store the class cookies such that they are available when we clone this RemoteView.
|
||
|
mClassCookies = parcel.copyClassCookies();
|
||
|
mCollectionCache = new RemoteCollectionCache(parcel);
|
||
|
} else {
|
||
|
configureAsChild(rootData);
|
||
|
}
|
||
|
|
||
|
if (mode == MODE_NORMAL) {
|
||
|
mApplication = parcel.readTypedObject(ApplicationInfo.CREATOR);
|
||
|
mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel);
|
||
|
mLayoutId = parcel.readInt();
|
||
|
mViewId = parcel.readInt();
|
||
|
mLightBackgroundLayoutId = parcel.readInt();
|
||
|
|
||
|
readActionsFromParcel(parcel, depth);
|
||
|
} else if (mode == MODE_HAS_SIZED_REMOTEVIEWS) {
|
||
|
int numViews = parcel.readInt();
|
||
|
if (numViews > MAX_INIT_VIEW_COUNT) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"Too many views in mapping from size to RemoteViews.");
|
||
|
}
|
||
|
List<RemoteViews> remoteViews = new ArrayList<>(numViews);
|
||
|
for (int i = 0; i < numViews; i++) {
|
||
|
RemoteViews view = new RemoteViews(parcel, getHierarchyRootData(), info, depth);
|
||
|
info = view.mApplication;
|
||
|
remoteViews.add(view);
|
||
|
}
|
||
|
initializeSizedRemoteViews(remoteViews.iterator());
|
||
|
RemoteViews smallestView = findSmallestRemoteView();
|
||
|
mApplication = smallestView.mApplication;
|
||
|
mLayoutId = smallestView.mLayoutId;
|
||
|
mViewId = smallestView.mViewId;
|
||
|
mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId;
|
||
|
} else {
|
||
|
// MODE_HAS_LANDSCAPE_AND_PORTRAIT
|
||
|
mLandscape = new RemoteViews(parcel, getHierarchyRootData(), info, depth);
|
||
|
mPortrait =
|
||
|
new RemoteViews(parcel, getHierarchyRootData(), mLandscape.mApplication, depth);
|
||
|
mApplication = mPortrait.mApplication;
|
||
|
mLayoutId = mPortrait.mLayoutId;
|
||
|
mViewId = mPortrait.mViewId;
|
||
|
mLightBackgroundLayoutId = mPortrait.mLightBackgroundLayoutId;
|
||
|
}
|
||
|
mApplyFlags = parcel.readInt();
|
||
|
mProviderInstanceId = parcel.readLong();
|
||
|
mHasDrawInstructions = parcel.readBoolean();
|
||
|
|
||
|
// Ensure that all descendants have their caches set up recursively.
|
||
|
if (mIsRoot) {
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void readActionsFromParcel(Parcel parcel, int depth) {
|
||
|
int count = parcel.readInt();
|
||
|
if (count > 0) {
|
||
|
mActions = new ArrayList<>(count);
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
mActions.add(getActionFromParcel(parcel, depth));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private Action getActionFromParcel(Parcel parcel, int depth) {
|
||
|
int tag = parcel.readInt();
|
||
|
switch (tag) {
|
||
|
case SET_ON_CLICK_RESPONSE_TAG:
|
||
|
return new SetOnClickResponse(parcel);
|
||
|
case SET_DRAWABLE_TINT_TAG:
|
||
|
return new SetDrawableTint(parcel);
|
||
|
case REFLECTION_ACTION_TAG:
|
||
|
return new ReflectionAction(parcel);
|
||
|
case VIEW_GROUP_ACTION_ADD_TAG:
|
||
|
return new ViewGroupActionAdd(parcel, mApplication, depth);
|
||
|
case VIEW_GROUP_ACTION_REMOVE_TAG:
|
||
|
return new ViewGroupActionRemove(parcel);
|
||
|
case VIEW_CONTENT_NAVIGATION_TAG:
|
||
|
return new ViewContentNavigation(parcel);
|
||
|
case SET_EMPTY_VIEW_ACTION_TAG:
|
||
|
return new SetEmptyView(parcel);
|
||
|
case SET_PENDING_INTENT_TEMPLATE_TAG:
|
||
|
return new SetPendingIntentTemplate(parcel);
|
||
|
case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG:
|
||
|
return new SetRemoteViewsAdapterIntent(parcel);
|
||
|
case TEXT_VIEW_DRAWABLE_ACTION_TAG:
|
||
|
return new TextViewDrawableAction(parcel);
|
||
|
case TEXT_VIEW_SIZE_ACTION_TAG:
|
||
|
return new TextViewSizeAction(parcel);
|
||
|
case VIEW_PADDING_ACTION_TAG:
|
||
|
return new ViewPaddingAction(parcel);
|
||
|
case BITMAP_REFLECTION_ACTION_TAG:
|
||
|
return new BitmapReflectionAction(parcel);
|
||
|
case SET_REMOTE_INPUTS_ACTION_TAG:
|
||
|
return new SetRemoteInputsAction(parcel);
|
||
|
case LAYOUT_PARAM_ACTION_TAG:
|
||
|
return new LayoutParamAction(parcel);
|
||
|
case SET_RIPPLE_DRAWABLE_COLOR_TAG:
|
||
|
return new SetRippleDrawableColor(parcel);
|
||
|
case SET_INT_TAG_TAG:
|
||
|
return new SetIntTagAction(parcel);
|
||
|
case REMOVE_FROM_PARENT_ACTION_TAG:
|
||
|
return new RemoveFromParentAction(parcel);
|
||
|
case RESOURCE_REFLECTION_ACTION_TAG:
|
||
|
return new ResourceReflectionAction(parcel);
|
||
|
case COMPLEX_UNIT_DIMENSION_REFLECTION_ACTION_TAG:
|
||
|
return new ComplexUnitDimensionReflectionAction(parcel);
|
||
|
case SET_COMPOUND_BUTTON_CHECKED_TAG:
|
||
|
return new SetCompoundButtonCheckedAction(parcel);
|
||
|
case SET_RADIO_GROUP_CHECKED:
|
||
|
return new SetRadioGroupCheckedAction(parcel);
|
||
|
case SET_VIEW_OUTLINE_RADIUS_TAG:
|
||
|
return new SetViewOutlinePreferredRadiusAction(parcel);
|
||
|
case SET_ON_CHECKED_CHANGE_RESPONSE_TAG:
|
||
|
return new SetOnCheckedChangeResponse(parcel);
|
||
|
case NIGHT_MODE_REFLECTION_ACTION_TAG:
|
||
|
return new NightModeReflectionAction(parcel);
|
||
|
case SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG:
|
||
|
return new SetRemoteCollectionItemListAdapterAction(parcel);
|
||
|
case ATTRIBUTE_REFLECTION_ACTION_TAG:
|
||
|
return new AttributeReflectionAction(parcel);
|
||
|
case SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG:
|
||
|
return new SetOnStylusHandwritingResponse(parcel);
|
||
|
case SET_DRAW_INSTRUCTION_TAG:
|
||
|
return new SetDrawInstructionAction(parcel);
|
||
|
default:
|
||
|
throw new ActionException("Tag " + tag + " not found");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a deep copy of the RemoteViews object. The RemoteView may not be
|
||
|
* attached to another RemoteView -- it must be the root of a hierarchy.
|
||
|
*
|
||
|
* @deprecated use {@link #RemoteViews(RemoteViews)} instead.
|
||
|
* @throws IllegalStateException if this is not the root of a RemoteView
|
||
|
* hierarchy
|
||
|
*/
|
||
|
@Override
|
||
|
@Deprecated
|
||
|
public RemoteViews clone() {
|
||
|
Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. "
|
||
|
+ "May only clone the root of a RemoteView hierarchy.");
|
||
|
|
||
|
return new RemoteViews(this);
|
||
|
}
|
||
|
|
||
|
public String getPackage() {
|
||
|
return (mApplication != null) ? mApplication.packageName : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the layout id of the root layout associated with this RemoteViews. In the case
|
||
|
* that the RemoteViews has both a landscape and portrait root, this will return the layout
|
||
|
* id associated with the portrait layout.
|
||
|
*
|
||
|
* @return the layout id.
|
||
|
*/
|
||
|
public int getLayoutId() {
|
||
|
return hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT) && (mLightBackgroundLayoutId != 0)
|
||
|
? mLightBackgroundLayoutId : mLayoutId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the root of the hierarchy and then recursively traverses the tree to update the root
|
||
|
* and populate caches for all descendants.
|
||
|
*/
|
||
|
private void configureAsChild(@NonNull HierarchyRootData rootData) {
|
||
|
mIsRoot = false;
|
||
|
mBitmapCache = rootData.mBitmapCache;
|
||
|
mCollectionCache = rootData.mRemoteCollectionCache;
|
||
|
mApplicationInfoCache = rootData.mApplicationInfoCache;
|
||
|
mClassCookies = rootData.mClassCookies;
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Recursively traverses the tree to update the root and populate caches for all descendants.
|
||
|
*/
|
||
|
private void configureDescendantsAsChildren() {
|
||
|
// Before propagating down the tree, replace our application from the root application info
|
||
|
// cache, to ensure the same instance is present throughout the hierarchy to allow for
|
||
|
// squashing.
|
||
|
mApplication = mApplicationInfoCache.getOrPut(mApplication);
|
||
|
|
||
|
HierarchyRootData rootData = getHierarchyRootData();
|
||
|
if (hasSizedRemoteViews()) {
|
||
|
for (RemoteViews remoteView : mSizedRemoteViews) {
|
||
|
remoteView.configureAsChild(rootData);
|
||
|
}
|
||
|
} else if (hasLandscapeAndPortraitLayouts()) {
|
||
|
mLandscape.configureAsChild(rootData);
|
||
|
mPortrait.configureAsChild(rootData);
|
||
|
} else {
|
||
|
if (mActions != null) {
|
||
|
for (Action action : mActions) {
|
||
|
action.setHierarchyRootData(rootData);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Recreates caches at the root level of the hierarchy, then recursively populates the caches
|
||
|
* down the hierarchy.
|
||
|
*/
|
||
|
private void reconstructCaches() {
|
||
|
if (!mIsRoot) return;
|
||
|
mBitmapCache = new BitmapCache();
|
||
|
mApplicationInfoCache = new ApplicationInfoCache();
|
||
|
mApplication = mApplicationInfoCache.getOrPut(mApplication);
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns an estimate of the bitmap heap memory usage for this RemoteViews.
|
||
|
*/
|
||
|
/** @hide */
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
public int estimateMemoryUsage() {
|
||
|
return mBitmapCache.getBitmapMemory();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add an action to be executed on the remote side when apply is called.
|
||
|
*
|
||
|
* @param a The action to add
|
||
|
*/
|
||
|
private void addAction(Action a) {
|
||
|
if (hasMultipleLayouts()) {
|
||
|
throw new RuntimeException("RemoteViews specifying separate layouts for orientation"
|
||
|
+ " or size cannot be modified. Instead, fully configure each layouts"
|
||
|
+ " individually before constructing the combined layout.");
|
||
|
}
|
||
|
if (mActions == null) {
|
||
|
mActions = new ArrayList<>();
|
||
|
}
|
||
|
mActions.add(a);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ViewGroup#addView(View)} after inflating the
|
||
|
* given {@link RemoteViews}. This allows users to build "nested"
|
||
|
* {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may
|
||
|
* recycle layouts, use {@link #removeAllViews(int)} to clear any existing
|
||
|
* children.
|
||
|
*
|
||
|
* @param viewId The id of the parent {@link ViewGroup} to add child into.
|
||
|
* @param nestedView {@link RemoteViews} that describes the child.
|
||
|
*/
|
||
|
public void addView(@IdRes int viewId, RemoteViews nestedView) {
|
||
|
// Clear all children when nested views omitted
|
||
|
addAction(nestedView == null
|
||
|
? new ViewGroupActionRemove(viewId)
|
||
|
: new ViewGroupActionAdd(viewId, nestedView));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ViewGroup#addView(View)} after inflating the given
|
||
|
* {@link RemoteViews}. If the {@link RemoteViews} may be re-inflated or updated,
|
||
|
* {@link #removeAllViews(int)} must be called on the same {@code viewId
|
||
|
* } before the first call to this method for the behavior of this method to be predictable.
|
||
|
*
|
||
|
* The {@code stableId} will be used to identify a potential view to recycled when the remote
|
||
|
* view is inflated. Views can be re-used if inserted in the same order, potentially with
|
||
|
* some views appearing / disappearing. To be recycled the view must not change the layout
|
||
|
* used to inflate it or its view id (see {@link RemoteViews#RemoteViews(String, int, int)}).
|
||
|
*
|
||
|
* Note: if a view is re-used, all the actions will be re-applied on it. However, its properties
|
||
|
* are not reset, so what was applied in previous round will have an effect. As a view may be
|
||
|
* re-created at any time by the host, the RemoteViews should not rely on keeping information
|
||
|
* from previous applications and always re-set all the properties they need.
|
||
|
*
|
||
|
* @param viewId The id of the parent {@link ViewGroup} to add child into.
|
||
|
* @param nestedView {@link RemoteViews} that describes the child.
|
||
|
* @param stableId An id that is stable across different versions of RemoteViews.
|
||
|
*/
|
||
|
public void addStableView(@IdRes int viewId, @NonNull RemoteViews nestedView, int stableId) {
|
||
|
addAction(new ViewGroupActionAdd(viewId, nestedView, -1 /* index */, stableId));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ViewGroup#addView(View, int)} after inflating the
|
||
|
* given {@link RemoteViews}.
|
||
|
*
|
||
|
* @param viewId The id of the parent {@link ViewGroup} to add the child into.
|
||
|
* @param nestedView {@link RemoteViews} of the child to add.
|
||
|
* @param index The position at which to add the child.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
public void addView(@IdRes int viewId, RemoteViews nestedView, int index) {
|
||
|
addAction(new ViewGroupActionAdd(viewId, nestedView, index));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ViewGroup#removeAllViews()}.
|
||
|
*
|
||
|
* @param viewId The id of the parent {@link ViewGroup} to remove all
|
||
|
* children from.
|
||
|
*/
|
||
|
public void removeAllViews(@IdRes int viewId) {
|
||
|
addAction(new ViewGroupActionRemove(viewId));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes all views in the {@link ViewGroup} specified by the {@code viewId} except for any
|
||
|
* child that has the {@code viewIdToKeep} as its id.
|
||
|
*
|
||
|
* @param viewId The id of the parent {@link ViewGroup} to remove children from.
|
||
|
* @param viewIdToKeep The id of a child that should not be removed.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void removeAllViewsExceptId(@IdRes int viewId, @IdRes int viewIdToKeep) {
|
||
|
addAction(new ViewGroupActionRemove(viewId, viewIdToKeep));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes the {@link View} specified by the {@code viewId} from its parent {@link ViewManager}.
|
||
|
* This will do nothing if the viewId specifies the root view of this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the {@link View} to remove from its parent.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void removeFromParent(@IdRes int viewId) {
|
||
|
addAction(new RemoveFromParentAction(viewId));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link AdapterViewAnimator#showNext()}
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()}
|
||
|
* @deprecated As RemoteViews may be reapplied frequently, it is preferable to call
|
||
|
* {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change
|
||
|
* unexpectedly.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void showNext(@IdRes int viewId) {
|
||
|
addAction(new ViewContentNavigation(viewId, true /* next */));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link AdapterViewAnimator#showPrevious()}
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()}
|
||
|
* @deprecated As RemoteViews may be reapplied frequently, it is preferable to call
|
||
|
* {@link #setDisplayedChild(int, int)} to ensure that the adapter index does not change
|
||
|
* unexpectedly.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void showPrevious(@IdRes int viewId) {
|
||
|
addAction(new ViewContentNavigation(viewId, false /* next */));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link AdapterViewAnimator#setDisplayedChild(int)}
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call
|
||
|
* {@link AdapterViewAnimator#setDisplayedChild(int)}
|
||
|
*/
|
||
|
public void setDisplayedChild(@IdRes int viewId, int childIndex) {
|
||
|
setInt(viewId, "setDisplayedChild", childIndex);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link View#setVisibility(int)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose visibility should change
|
||
|
* @param visibility The new visibility for the view
|
||
|
*/
|
||
|
public void setViewVisibility(@IdRes int viewId, @View.Visibility int visibility) {
|
||
|
setInt(viewId, "setVisibility", visibility);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link TextView#setText(CharSequence)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose text should change
|
||
|
* @param text The new text for the view
|
||
|
*/
|
||
|
public void setTextViewText(@IdRes int viewId, CharSequence text) {
|
||
|
setCharSequence(viewId, "setText", text);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link TextView#setTextSize(int, float)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose text size should change
|
||
|
* @param units The units of size (e.g. COMPLEX_UNIT_SP)
|
||
|
* @param size The size of the text
|
||
|
*/
|
||
|
public void setTextViewTextSize(@IdRes int viewId, int units, float size) {
|
||
|
addAction(new TextViewSizeAction(viewId, units, size));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link TextView#setCompoundDrawablesWithIntrinsicBounds(int, int, int, int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text should change
|
||
|
* @param left The id of a drawable to place to the left of the text, or 0
|
||
|
* @param top The id of a drawable to place above the text, or 0
|
||
|
* @param right The id of a drawable to place to the right of the text, or 0
|
||
|
* @param bottom The id of a drawable to place below the text, or 0
|
||
|
*/
|
||
|
public void setTextViewCompoundDrawables(@IdRes int viewId, @DrawableRes int left,
|
||
|
@DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
|
||
|
addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link
|
||
|
* TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text should change
|
||
|
* @param start The id of a drawable to place before the text (relative to the
|
||
|
* layout direction), or 0
|
||
|
* @param top The id of a drawable to place above the text, or 0
|
||
|
* @param end The id of a drawable to place after the text, or 0
|
||
|
* @param bottom The id of a drawable to place below the text, or 0
|
||
|
*/
|
||
|
public void setTextViewCompoundDrawablesRelative(@IdRes int viewId, @DrawableRes int start,
|
||
|
@DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
|
||
|
addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link
|
||
|
* TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
|
||
|
* using the drawables yielded by {@link Icon#loadDrawable(Context)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text should change
|
||
|
* @param left an Icon to place to the left of the text, or 0
|
||
|
* @param top an Icon to place above the text, or 0
|
||
|
* @param right an Icon to place to the right of the text, or 0
|
||
|
* @param bottom an Icon to place below the text, or 0
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setTextViewCompoundDrawables(@IdRes int viewId,
|
||
|
Icon left, Icon top, Icon right, Icon bottom) {
|
||
|
addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link
|
||
|
* TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
|
||
|
* using the drawables yielded by {@link Icon#loadDrawable(Context)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text should change
|
||
|
* @param start an Icon to place before the text (relative to the
|
||
|
* layout direction), or 0
|
||
|
* @param top an Icon to place above the text, or 0
|
||
|
* @param end an Icon to place after the text, or 0
|
||
|
* @param bottom an Icon to place below the text, or 0
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setTextViewCompoundDrawablesRelative(@IdRes int viewId,
|
||
|
Icon start, Icon top, Icon end, Icon bottom) {
|
||
|
addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ImageView#setImageResource(int)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose drawable should change
|
||
|
* @param srcId The new resource id for the drawable
|
||
|
*/
|
||
|
public void setImageViewResource(@IdRes int viewId, @DrawableRes int srcId) {
|
||
|
setInt(viewId, "setImageResource", srcId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ImageView#setImageURI(Uri)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose drawable should change
|
||
|
* @param uri The Uri for the image
|
||
|
*/
|
||
|
public void setImageViewUri(@IdRes int viewId, Uri uri) {
|
||
|
setUri(viewId, "setImageURI", uri);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ImageView#setImageBitmap(Bitmap)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose bitmap should change
|
||
|
* @param bitmap The new Bitmap for the drawable
|
||
|
*/
|
||
|
public void setImageViewBitmap(@IdRes int viewId, Bitmap bitmap) {
|
||
|
setBitmap(viewId, "setImageBitmap", bitmap);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ImageView#setImageIcon(Icon)}
|
||
|
*
|
||
|
* @param viewId The id of the view whose bitmap should change
|
||
|
* @param icon The new Icon for the ImageView
|
||
|
*/
|
||
|
public void setImageViewIcon(@IdRes int viewId, Icon icon) {
|
||
|
setIcon(viewId, "setImageIcon", icon);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link AdapterView#setEmptyView(View)}
|
||
|
*
|
||
|
* @param viewId The id of the view on which to set the empty view
|
||
|
* @param emptyViewId The view id of the empty view
|
||
|
*/
|
||
|
public void setEmptyView(@IdRes int viewId, @IdRes int emptyViewId) {
|
||
|
addAction(new SetEmptyView(viewId, emptyViewId));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link Chronometer#setBase Chronometer.setBase},
|
||
|
* {@link Chronometer#setFormat Chronometer.setFormat},
|
||
|
* and {@link Chronometer#start Chronometer.start()} or
|
||
|
* {@link Chronometer#stop Chronometer.stop()}.
|
||
|
*
|
||
|
* @param viewId The id of the {@link Chronometer} to change
|
||
|
* @param base The time at which the timer would have read 0:00. This
|
||
|
* time should be based off of
|
||
|
* {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}.
|
||
|
* @param format The Chronometer format string, or null to
|
||
|
* simply display the timer value.
|
||
|
* @param started True if you want the clock to be started, false if not.
|
||
|
*
|
||
|
* @see #setChronometerCountDown(int, boolean)
|
||
|
*/
|
||
|
public void setChronometer(@IdRes int viewId, long base, String format, boolean started) {
|
||
|
setLong(viewId, "setBase", base);
|
||
|
setString(viewId, "setFormat", format);
|
||
|
setBoolean(viewId, "setStarted", started);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link Chronometer#setCountDown(boolean) Chronometer.setCountDown} on
|
||
|
* the chronometer with the given viewId.
|
||
|
*
|
||
|
* @param viewId The id of the {@link Chronometer} to change
|
||
|
* @param isCountDown True if you want the chronometer to count down to base instead of
|
||
|
* counting up.
|
||
|
*/
|
||
|
public void setChronometerCountDown(@IdRes int viewId, boolean isCountDown) {
|
||
|
setBoolean(viewId, "setCountDown", isCountDown);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax},
|
||
|
* {@link ProgressBar#setProgress ProgressBar.setProgress}, and
|
||
|
* {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate}
|
||
|
*
|
||
|
* If indeterminate is true, then the values for max and progress are ignored.
|
||
|
*
|
||
|
* @param viewId The id of the {@link ProgressBar} to change
|
||
|
* @param max The 100% value for the progress bar
|
||
|
* @param progress The current value of the progress bar.
|
||
|
* @param indeterminate True if the progress bar is indeterminate,
|
||
|
* false if not.
|
||
|
*/
|
||
|
public void setProgressBar(@IdRes int viewId, int max, int progress,
|
||
|
boolean indeterminate) {
|
||
|
setBoolean(viewId, "setIndeterminate", indeterminate);
|
||
|
if (!indeterminate) {
|
||
|
setInt(viewId, "setMax", max);
|
||
|
setInt(viewId, "setProgress", progress);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
|
||
|
* to launch the provided {@link PendingIntent}. The source bounds
|
||
|
* ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the clicked
|
||
|
* view in screen space.
|
||
|
* Note that any activity options associated with the mPendingIntent may get overridden
|
||
|
* before starting the intent.
|
||
|
*
|
||
|
* When setting the on-click action of items within collections (eg. {@link ListView},
|
||
|
* {@link StackView} etc.), this method will not work. Instead, use {@link
|
||
|
* RemoteViews#setPendingIntentTemplate(int, PendingIntent)} in conjunction with
|
||
|
* {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
|
||
|
*
|
||
|
* @param viewId The id of the view that will trigger the {@link PendingIntent} when clicked
|
||
|
* @param pendingIntent The {@link PendingIntent} to send when user clicks
|
||
|
*/
|
||
|
public void setOnClickPendingIntent(@IdRes int viewId, PendingIntent pendingIntent) {
|
||
|
setOnClickResponse(viewId, RemoteResponse.fromPendingIntent(pendingIntent));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent of calling
|
||
|
* {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
|
||
|
* to launch the provided {@link RemoteResponse}.
|
||
|
*
|
||
|
* @param viewId The id of the view that will trigger the {@link RemoteResponse} when clicked
|
||
|
* @param response The {@link RemoteResponse} to send when user clicks
|
||
|
*/
|
||
|
public void setOnClickResponse(@IdRes int viewId, @NonNull RemoteResponse response) {
|
||
|
addAction(new SetOnClickResponse(viewId, response));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
|
||
|
* costly to set PendingIntents on the individual items, and is hence not recommended. Instead
|
||
|
* this method should be used to set a single PendingIntent template on the collection, and
|
||
|
* individual items can differentiate their on-click behavior using
|
||
|
* {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
|
||
|
*
|
||
|
* @param viewId The id of the collection who's children will use this PendingIntent template
|
||
|
* when clicked
|
||
|
* @param pendingIntentTemplate The {@link PendingIntent} to be combined with extras specified
|
||
|
* by a child of viewId and executed when that child is clicked
|
||
|
*/
|
||
|
public void setPendingIntentTemplate(@IdRes int viewId, PendingIntent pendingIntentTemplate) {
|
||
|
if (hasDrawInstructions()) {
|
||
|
getPendingIntentTemplate().set(viewId, pendingIntentTemplate);
|
||
|
tryAddRemoteResponse(viewId);
|
||
|
} else {
|
||
|
addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
|
||
|
* costly to set PendingIntents on the individual items, and is hence not recommended. Instead
|
||
|
* a single PendingIntent template can be set on the collection, see {@link
|
||
|
* RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click
|
||
|
* action of a given item can be distinguished by setting a fillInIntent on that item. The
|
||
|
* fillInIntent is then combined with the PendingIntent template in order to determine the final
|
||
|
* intent which will be executed when the item is clicked. This works as follows: any fields
|
||
|
* which are left blank in the PendingIntent template, but are provided by the fillInIntent
|
||
|
* will be overwritten, and the resulting PendingIntent will be used. The rest
|
||
|
* of the PendingIntent template will then be filled in with the associated fields that are
|
||
|
* set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to set the fillInIntent
|
||
|
* @param fillInIntent The intent which will be combined with the parent's PendingIntent
|
||
|
* in order to determine the on-click behavior of the view specified by viewId
|
||
|
*/
|
||
|
public void setOnClickFillInIntent(@IdRes int viewId, Intent fillInIntent) {
|
||
|
if (hasDrawInstructions()) {
|
||
|
getFillInIntent().set(viewId, fillInIntent);
|
||
|
tryAddRemoteResponse(viewId);
|
||
|
} else {
|
||
|
setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling
|
||
|
* {@link android.widget.CompoundButton#setOnCheckedChangeListener(
|
||
|
* android.widget.CompoundButton.OnCheckedChangeListener)}
|
||
|
* to launch the provided {@link RemoteResponse}.
|
||
|
*
|
||
|
* The intent will be filled with the current checked state of the view at the key
|
||
|
* {@link #EXTRA_CHECKED}.
|
||
|
*
|
||
|
* The {@link RemoteResponse} will not be launched in response to check changes arising from
|
||
|
* {@link #setCompoundButtonChecked(int, boolean)} or {@link #setRadioGroupChecked(int, int)}
|
||
|
* usages.
|
||
|
*
|
||
|
* The {@link RemoteResponse} must be created using
|
||
|
* {@link RemoteResponse#fromFillInIntent(Intent)} in conjunction with
|
||
|
* {@link RemoteViews#setPendingIntentTemplate(int, PendingIntent)} for items inside
|
||
|
* collections (eg. {@link ListView}, {@link StackView} etc.).
|
||
|
*
|
||
|
* Otherwise, create the {@link RemoteResponse} using
|
||
|
* {@link RemoteResponse#fromPendingIntent(PendingIntent)}.
|
||
|
*
|
||
|
* @param viewId The id of the view that will trigger the {@link PendingIntent} when checked
|
||
|
* state changes.
|
||
|
* @param response The {@link RemoteResponse} to send when the checked state changes.
|
||
|
*/
|
||
|
public void setOnCheckedChangeResponse(
|
||
|
@IdRes int viewId,
|
||
|
@NonNull RemoteResponse response) {
|
||
|
addAction(
|
||
|
new SetOnCheckedChangeResponse(
|
||
|
viewId,
|
||
|
response.setInteractionType(
|
||
|
RemoteResponse.INTERACTION_TYPE_CHECKED_CHANGE)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link View#setHandwritingDelegatorCallback(Runnable)} to send the
|
||
|
* provided {@link PendingIntent}.
|
||
|
*
|
||
|
* <p>A common use case is a remote view which looks like a text editor but does not actually
|
||
|
* support text editing itself, and clicking on the remote view launches an activity containing
|
||
|
* an EditText. To support handwriting initiation in this case, this method can be called on the
|
||
|
* remote view to configure it as a handwriting delegator, meaning that stylus movement on the
|
||
|
* remote view triggers a {@link PendingIntent} and starts handwriting mode for the delegate
|
||
|
* EditText. The {@link PendingIntent} is typically the same as the one passed to {@link
|
||
|
* #setOnClickPendingIntent} which launches the activity containing the EditText. The EditText
|
||
|
* should call {@link View#setIsHandwritingDelegate} to set it as a delegate, and also use
|
||
|
* {@link View#setAllowedHandwritingDelegatorPackage} or {@link
|
||
|
* android.view.inputmethod.InputMethodManager#HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED}
|
||
|
* if necessary to support delegators from the package displaying the remote view.
|
||
|
*
|
||
|
* @param viewId identifier of the view that will trigger the {@link PendingIntent} when a
|
||
|
* stylus {@link MotionEvent} occurs within the view's bounds
|
||
|
* @param pendingIntent the {@link PendingIntent} to send, or {@code null} to clear the
|
||
|
* handwriting delegation
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR)
|
||
|
public void setOnStylusHandwritingPendingIntent(
|
||
|
@IdRes int viewId, @Nullable PendingIntent pendingIntent) {
|
||
|
addAction(new SetOnStylusHandwritingResponse(viewId, pendingIntent));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling
|
||
|
* {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
|
||
|
* on the {@link Drawable} of a given view.
|
||
|
* <p>
|
||
|
*
|
||
|
* @param viewId The id of the view that contains the target
|
||
|
* {@link Drawable}
|
||
|
* @param targetBackground If true, apply these parameters to the
|
||
|
* {@link Drawable} returned by
|
||
|
* {@link android.view.View#getBackground()}. Otherwise, assume
|
||
|
* the target view is an {@link ImageView} and apply them to
|
||
|
* {@link ImageView#getDrawable()}.
|
||
|
* @param colorFilter Specify a color for a
|
||
|
* {@link android.graphics.ColorFilter} for this drawable. This will be ignored if
|
||
|
* {@code mode} is {@code null}.
|
||
|
* @param mode Specify a PorterDuff mode for this drawable, or null to leave
|
||
|
* unchanged.
|
||
|
*/
|
||
|
public void setDrawableTint(@IdRes int viewId, boolean targetBackground,
|
||
|
@ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) {
|
||
|
addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling
|
||
|
* {@link RippleDrawable#setColor(ColorStateList)} on the {@link Drawable} of a given view,
|
||
|
* assuming it's a {@link RippleDrawable}.
|
||
|
* <p>
|
||
|
*
|
||
|
* @param viewId The id of the view that contains the target
|
||
|
* {@link RippleDrawable}
|
||
|
* @param colorStateList Specify a color for a
|
||
|
* {@link ColorStateList} for this drawable.
|
||
|
*/
|
||
|
public void setRippleDrawableColor(@IdRes int viewId, ColorStateList colorStateList) {
|
||
|
addAction(new SetRippleDrawableColor(viewId, colorStateList));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose tint should change
|
||
|
* @param tint the tint to apply, may be {@code null} to clear tint
|
||
|
*/
|
||
|
public void setProgressTintList(@IdRes int viewId, ColorStateList tint) {
|
||
|
addAction(new ReflectionAction(viewId, "setProgressTintList",
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, tint));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose tint should change
|
||
|
* @param tint the tint to apply, may be {@code null} to clear tint
|
||
|
*/
|
||
|
public void setProgressBackgroundTintList(@IdRes int viewId, ColorStateList tint) {
|
||
|
addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList",
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, tint));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose tint should change
|
||
|
* @param tint the tint to apply, may be {@code null} to clear tint
|
||
|
*/
|
||
|
public void setProgressIndeterminateTintList(@IdRes int viewId, ColorStateList tint) {
|
||
|
addAction(new ReflectionAction(viewId, "setIndeterminateTintList",
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, tint));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.widget.TextView#setTextColor(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text color should change
|
||
|
* @param color Sets the text color for all the states (normal, selected,
|
||
|
* focused) to be this color.
|
||
|
*/
|
||
|
public void setTextColor(@IdRes int viewId, @ColorInt int color) {
|
||
|
setInt(viewId, "setTextColor", color);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Equivalent to calling {@link android.widget.TextView#setTextColor(ColorStateList)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose text color should change
|
||
|
* @param colors the text colors to set
|
||
|
*/
|
||
|
public void setTextColor(@IdRes int viewId, ColorStateList colors) {
|
||
|
addAction(new ReflectionAction(viewId, "setTextColor",
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, colors));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
|
||
|
*
|
||
|
* @param appWidgetId The id of the app widget which contains the specified view. (This
|
||
|
* parameter is ignored in this deprecated method)
|
||
|
* @param viewId The id of the {@link AdapterView}
|
||
|
* @param intent The intent of the service which will be
|
||
|
* providing data to the RemoteViewsAdapter
|
||
|
* @deprecated This method has been deprecated. See
|
||
|
* {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)}
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void setRemoteAdapter(int appWidgetId, @IdRes int viewId, Intent intent) {
|
||
|
setRemoteAdapter(viewId, intent);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
|
||
|
* Can only be used for App Widgets.
|
||
|
*
|
||
|
* @param viewId The id of the {@link AdapterView}
|
||
|
* @param intent The intent of the service which will be
|
||
|
* providing data to the RemoteViewsAdapter
|
||
|
* @deprecated use
|
||
|
* {@link #setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} instead
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void setRemoteAdapter(@IdRes int viewId, Intent intent) {
|
||
|
if (remoteAdapterConversion()) {
|
||
|
addAction(new SetRemoteCollectionItemListAdapterAction(viewId, intent));
|
||
|
} else {
|
||
|
addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
|
||
|
* ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
|
||
|
* This is a simpler but less flexible approach to populating collection widgets. Its use is
|
||
|
* encouraged for most scenarios, as long as the total memory within the list of RemoteViews
|
||
|
* is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
|
||
|
* RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
|
||
|
* possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
|
||
|
*
|
||
|
* This API is supported in the compatibility library for previous API levels, see
|
||
|
* RemoteViewsCompat.
|
||
|
*
|
||
|
* @param viewId The id of the {@link AdapterView}
|
||
|
* @param list The list of RemoteViews which will populate the view specified by viewId.
|
||
|
* @param viewTypeCount The maximum number of unique layout id's used to construct the list of
|
||
|
* RemoteViews. This count cannot change during the life-cycle of a given widget, so this
|
||
|
* parameter should account for the maximum possible number of types that may appear in the
|
||
|
* See {@link Adapter#getViewTypeCount()}.
|
||
|
*
|
||
|
* @hide
|
||
|
* @deprecated this appears to have no users outside of UnsupportedAppUsage?
|
||
|
*/
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
@Deprecated
|
||
|
public void setRemoteAdapter(@IdRes int viewId, ArrayList<RemoteViews> list,
|
||
|
int viewTypeCount) {
|
||
|
RemoteCollectionItems.Builder b = new RemoteCollectionItems.Builder();
|
||
|
for (int i = 0; i < list.size(); i++) {
|
||
|
b.addItem(i, list.get(i));
|
||
|
}
|
||
|
setRemoteAdapter(viewId, b.setViewTypeCount(viewTypeCount).build());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
|
||
|
* ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
|
||
|
* This is a simpler but less flexible approach to populating collection widgets. Its use is
|
||
|
* encouraged for most scenarios, as long as the total memory within the list of RemoteViews
|
||
|
* is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
|
||
|
* RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
|
||
|
* possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
|
||
|
*
|
||
|
* This API is supported in the compatibility library for previous API levels, see
|
||
|
* RemoteViewsCompat.
|
||
|
*
|
||
|
* @param viewId The id of the {@link AdapterView}.
|
||
|
* @param items The items to display in the {@link AdapterView}.
|
||
|
*/
|
||
|
public void setRemoteAdapter(@IdRes int viewId, @NonNull RemoteCollectionItems items) {
|
||
|
addAction(new SetRemoteCollectionItemListAdapterAction(viewId, items));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ListView#smoothScrollToPosition(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param position Scroll to this adapter position
|
||
|
*/
|
||
|
public void setScrollPosition(@IdRes int viewId, int position) {
|
||
|
setInt(viewId, "smoothScrollToPosition", position);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link ListView#smoothScrollByOffset(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param offset Scroll by this adapter position offset
|
||
|
*/
|
||
|
public void setRelativeScrollPosition(@IdRes int viewId, int offset) {
|
||
|
setInt(viewId, "smoothScrollByOffset", offset);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.view.View#setPadding(int, int, int, int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param left the left padding in pixels
|
||
|
* @param top the top padding in pixels
|
||
|
* @param right the right padding in pixels
|
||
|
* @param bottom the bottom padding in pixels
|
||
|
*/
|
||
|
public void setViewPadding(@IdRes int viewId,
|
||
|
@Px int left, @Px int top, @Px int right, @Px int bottom) {
|
||
|
addAction(new ViewPaddingAction(viewId, left, top, right, bottom));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
|
||
|
* Only works if the {@link View#getLayoutParams()} supports margins.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param type The margin being set e.g. {@link #MARGIN_END}
|
||
|
* @param dimen a dimension resource to apply to the margin, or 0 to clear the margin.
|
||
|
*/
|
||
|
public void setViewLayoutMarginDimen(@IdRes int viewId, @MarginType int type,
|
||
|
@DimenRes int dimen) {
|
||
|
addAction(new LayoutParamAction(viewId, type, dimen, VALUE_TYPE_RESOURCE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
|
||
|
* Only works if the {@link View#getLayoutParams()} supports margins.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param type The margin being set e.g. {@link #MARGIN_END}
|
||
|
* @param attr a dimension attribute to apply to the margin, or 0 to clear the margin.
|
||
|
*/
|
||
|
public void setViewLayoutMarginAttr(@IdRes int viewId, @MarginType int type,
|
||
|
@AttrRes int attr) {
|
||
|
addAction(new LayoutParamAction(viewId, type, attr, VALUE_TYPE_ATTRIBUTE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link MarginLayoutParams#setMarginEnd}.
|
||
|
* Only works if the {@link View#getLayoutParams()} supports margins.
|
||
|
*
|
||
|
* <p>NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0.
|
||
|
* Setting margins in pixels will behave poorly when the RemoteViews object is used on a
|
||
|
* display with a different density.
|
||
|
*
|
||
|
* @param viewId The id of the view to change
|
||
|
* @param type The margin being set e.g. {@link #MARGIN_END}
|
||
|
* @param value a value for the margin the given units.
|
||
|
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
|
||
|
*/
|
||
|
public void setViewLayoutMargin(@IdRes int viewId, @MarginType int type, float value,
|
||
|
@ComplexDimensionUnit int units) {
|
||
|
addAction(new LayoutParamAction(viewId, type, value, units));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} except that you may
|
||
|
* provide the value in any dimension units.
|
||
|
*
|
||
|
* <p>NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0,
|
||
|
* {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}.
|
||
|
* Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a
|
||
|
* display with a different density.
|
||
|
*
|
||
|
* @param width Width of the view in the given units
|
||
|
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
|
||
|
*/
|
||
|
public void setViewLayoutWidth(@IdRes int viewId, float width,
|
||
|
@ComplexDimensionUnit int units) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, width, units));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with
|
||
|
* the result of {@link Resources#getDimensionPixelSize(int)}.
|
||
|
*
|
||
|
* @param widthDimen the dimension resource for the view's width
|
||
|
*/
|
||
|
public void setViewLayoutWidthDimen(@IdRes int viewId, @DimenRes int widthDimen) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthDimen,
|
||
|
VALUE_TYPE_RESOURCE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width} with
|
||
|
* the value of the given attribute in the current theme.
|
||
|
*
|
||
|
* @param widthAttr the dimension attribute for the view's width
|
||
|
*/
|
||
|
public void setViewLayoutWidthAttr(@IdRes int viewId, @AttrRes int widthAttr) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, widthAttr,
|
||
|
VALUE_TYPE_ATTRIBUTE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} except that you may
|
||
|
* provide the value in any dimension units.
|
||
|
*
|
||
|
* <p>NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0,
|
||
|
* {@link ViewGroup.LayoutParams#WRAP_CONTENT}, or {@link ViewGroup.LayoutParams#MATCH_PARENT}.
|
||
|
* Setting actual sizes in pixels will behave poorly when the RemoteViews object is used on a
|
||
|
* display with a different density.
|
||
|
*
|
||
|
* @param height height of the view in the given units
|
||
|
* @param units The unit type of the value e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
|
||
|
*/
|
||
|
public void setViewLayoutHeight(@IdRes int viewId, float height,
|
||
|
@ComplexDimensionUnit int units) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, height, units));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with
|
||
|
* the result of {@link Resources#getDimensionPixelSize(int)}.
|
||
|
*
|
||
|
* @param heightDimen a dimen resource to read the height from.
|
||
|
*/
|
||
|
public void setViewLayoutHeightDimen(@IdRes int viewId, @DimenRes int heightDimen) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightDimen,
|
||
|
VALUE_TYPE_RESOURCE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to setting {@link android.view.ViewGroup.LayoutParams#height} with
|
||
|
* the value of the given attribute in the current theme.
|
||
|
*
|
||
|
* @param heightAttr a dimen attribute to read the height from.
|
||
|
*/
|
||
|
public void setViewLayoutHeightAttr(@IdRes int viewId, @AttrRes int heightAttr) {
|
||
|
addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_HEIGHT, heightAttr,
|
||
|
VALUE_TYPE_ATTRIBUTE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an OutlineProvider on the view whose corner radius is a dimension calculated using
|
||
|
* {@link TypedValue#applyDimension(int, float, DisplayMetrics)}.
|
||
|
*
|
||
|
* <p>NOTE: It is recommended to use {@link TypedValue#COMPLEX_UNIT_PX} only for 0.
|
||
|
* Setting margins in pixels will behave poorly when the RemoteViews object is used on a
|
||
|
* display with a different density.
|
||
|
*/
|
||
|
public void setViewOutlinePreferredRadius(
|
||
|
@IdRes int viewId, float radius, @ComplexDimensionUnit int units) {
|
||
|
addAction(new SetViewOutlinePreferredRadiusAction(viewId, radius, units));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an OutlineProvider on the view whose corner radius is a dimension resource with
|
||
|
* {@code resId}.
|
||
|
*/
|
||
|
public void setViewOutlinePreferredRadiusDimen(@IdRes int viewId, @DimenRes int resId) {
|
||
|
addAction(new SetViewOutlinePreferredRadiusAction(viewId, resId, VALUE_TYPE_RESOURCE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an OutlineProvider on the view whose corner radius is a dimension attribute with
|
||
|
* {@code attrId}.
|
||
|
*/
|
||
|
public void setViewOutlinePreferredRadiusAttr(@IdRes int viewId, @AttrRes int attrId) {
|
||
|
addAction(new SetViewOutlinePreferredRadiusAction(viewId, attrId, VALUE_TYPE_ATTRIBUTE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one boolean on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setBoolean(@IdRes int viewId, String methodName, boolean value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BOOLEAN, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one byte on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setByte(@IdRes int viewId, String methodName, byte value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BYTE, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one short on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setShort(@IdRes int viewId, String methodName, short value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.SHORT, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setInt(@IdRes int viewId, String methodName, int value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* Undefined resources will result in an exception, except 0 which will resolve to 0.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param dimenResource The resource to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setIntDimen(@IdRes int viewId, @NonNull String methodName,
|
||
|
@DimenRes int dimenResource) {
|
||
|
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
|
||
|
ResourceReflectionAction.DIMEN_RESOURCE, dimenResource));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the specified dimension at the time of inflation.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value of the dimension.
|
||
|
* @param unit The unit in which the value is specified.
|
||
|
*/
|
||
|
public void setIntDimen(@IdRes int viewId, @NonNull String methodName,
|
||
|
float value, @ComplexDimensionUnit int unit) {
|
||
|
addAction(new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.INT,
|
||
|
value, unit));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the theme attribute at the time the
|
||
|
* {@link RemoteViews} is (re-)applied.
|
||
|
*
|
||
|
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param dimenAttr The attribute to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setIntDimenAttr(@IdRes int viewId, @NonNull String methodName,
|
||
|
@AttrRes int dimenAttr) {
|
||
|
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT,
|
||
|
ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The Color will be resolved from the resources at the time the {@link RemoteViews} is (re-)
|
||
|
* applied.
|
||
|
*
|
||
|
* Undefined resources will result in an exception, except 0 which will resolve to 0.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param colorResource The resource to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setColor(@IdRes int viewId, @NonNull String methodName,
|
||
|
@ColorRes int colorResource) {
|
||
|
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
|
||
|
ResourceReflectionAction.COLOR_RESOURCE, colorResource));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The Color will be resolved from the theme attribute at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param colorAttribute The theme attribute to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setColorAttr(@IdRes int viewId, @NonNull String methodName,
|
||
|
@AttrRes int colorAttribute) {
|
||
|
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.INT,
|
||
|
AttributeReflectionAction.COLOR_RESOURCE, colorAttribute));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one int, a color, on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param notNight The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_NO}
|
||
|
* @param night The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_YES}
|
||
|
*/
|
||
|
public void setColorInt(
|
||
|
@IdRes int viewId,
|
||
|
@NonNull String methodName,
|
||
|
@ColorInt int notNight,
|
||
|
@ColorInt int night) {
|
||
|
addAction(
|
||
|
new NightModeReflectionAction(
|
||
|
viewId,
|
||
|
methodName,
|
||
|
BaseReflectionAction.INT,
|
||
|
notNight,
|
||
|
night));
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setColorStateList(@IdRes int viewId, @NonNull String methodName,
|
||
|
@Nullable ColorStateList value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.COLOR_STATE_LIST,
|
||
|
value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param notNight The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_NO}
|
||
|
* @param night The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_YES}
|
||
|
*/
|
||
|
public void setColorStateList(
|
||
|
@IdRes int viewId,
|
||
|
@NonNull String methodName,
|
||
|
@Nullable ColorStateList notNight,
|
||
|
@Nullable ColorStateList night) {
|
||
|
addAction(
|
||
|
new NightModeReflectionAction(
|
||
|
viewId,
|
||
|
methodName,
|
||
|
BaseReflectionAction.COLOR_STATE_LIST,
|
||
|
notNight,
|
||
|
night));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The ColorStateList will be resolved from the resources at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* Undefined resources will result in an exception, except 0 which will resolve to null.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param colorResource The resource to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setColorStateList(@IdRes int viewId, @NonNull String methodName,
|
||
|
@ColorRes int colorResource) {
|
||
|
addAction(new ResourceReflectionAction(viewId, methodName,
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE,
|
||
|
colorResource));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one ColorStateList on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The ColorStateList will be resolved from the theme attribute at the time the
|
||
|
* {@link RemoteViews} is (re-)applied.
|
||
|
*
|
||
|
* Unresolvable attributes will result in an exception, except 0 which will resolve to null.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param colorAttr The theme attribute to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setColorStateListAttr(@IdRes int viewId, @NonNull String methodName,
|
||
|
@AttrRes int colorAttr) {
|
||
|
addAction(new AttributeReflectionAction(viewId, methodName,
|
||
|
BaseReflectionAction.COLOR_STATE_LIST, ResourceReflectionAction.COLOR_RESOURCE,
|
||
|
colorAttr));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one long on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setLong(@IdRes int viewId, String methodName, long value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.LONG, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one float on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setFloat(@IdRes int viewId, String methodName, float value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one float, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* Undefined resources will result in an exception, except 0 which will resolve to 0f.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param dimenResource The resource to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setFloatDimen(@IdRes int viewId, @NonNull String methodName,
|
||
|
@DimenRes int dimenResource) {
|
||
|
addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT,
|
||
|
ResourceReflectionAction.DIMEN_RESOURCE, dimenResource));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one float, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the resources at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value of the dimension.
|
||
|
* @param unit The unit in which the value is specified.
|
||
|
*/
|
||
|
public void setFloatDimen(@IdRes int viewId, @NonNull String methodName,
|
||
|
float value, @ComplexDimensionUnit int unit) {
|
||
|
addAction(
|
||
|
new ComplexUnitDimensionReflectionAction(viewId, methodName, ReflectionAction.FLOAT,
|
||
|
value, unit));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one float, a size in pixels, on a view in the layout for this
|
||
|
* RemoteViews.
|
||
|
*
|
||
|
* The dimension will be resolved from the theme attribute at the time the {@link RemoteViews}
|
||
|
* is (re-)applied.
|
||
|
*
|
||
|
* Unresolvable attributes will result in an exception, except 0 which will resolve to 0f.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param dimenAttr The attribute to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setFloatDimenAttr(@IdRes int viewId, @NonNull String methodName,
|
||
|
@AttrRes int dimenAttr) {
|
||
|
addAction(new AttributeReflectionAction(viewId, methodName, BaseReflectionAction.FLOAT,
|
||
|
ResourceReflectionAction.DIMEN_RESOURCE, dimenAttr));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one double on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setDouble(@IdRes int viewId, String methodName, double value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.DOUBLE, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one char on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setChar(@IdRes int viewId, String methodName, char value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one String on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setString(@IdRes int viewId, String methodName, String value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.STRING, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setCharSequence(@IdRes int viewId, String methodName, CharSequence value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE,
|
||
|
value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The CharSequence will be resolved from the resources at the time the {@link RemoteViews} is
|
||
|
* (re-)applied.
|
||
|
*
|
||
|
* Undefined resources will result in an exception, except 0 which will resolve to null.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param stringResource The resource to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setCharSequence(@IdRes int viewId, @NonNull String methodName,
|
||
|
@StringRes int stringResource) {
|
||
|
addAction(
|
||
|
new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.CHAR_SEQUENCE,
|
||
|
ResourceReflectionAction.STRING_RESOURCE, stringResource));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* The CharSequence will be resolved from the theme attribute at the time the
|
||
|
* {@link RemoteViews} is (re-)applied.
|
||
|
*
|
||
|
* Unresolvable attributes will result in an exception, except 0 which will resolve to null.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param stringAttribute The attribute to resolve and pass as argument to the method.
|
||
|
*/
|
||
|
public void setCharSequenceAttr(@IdRes int viewId, @NonNull String methodName,
|
||
|
@AttrRes int stringAttribute) {
|
||
|
addAction(
|
||
|
new AttributeReflectionAction(viewId, methodName,
|
||
|
BaseReflectionAction.CHAR_SEQUENCE,
|
||
|
AttributeReflectionAction.STRING_RESOURCE, stringAttribute));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Uri on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setUri(@IdRes int viewId, String methodName, Uri value) {
|
||
|
if (value != null) {
|
||
|
// Resolve any filesystem path before sending remotely
|
||
|
value = value.getCanonicalUri();
|
||
|
if (StrictMode.vmFileUriExposureEnabled()) {
|
||
|
value.checkFileUriExposed("RemoteViews.setUri()");
|
||
|
}
|
||
|
}
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.URI, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Bitmap on a view in the layout for this RemoteViews.
|
||
|
* @more
|
||
|
* <p class="note">The bitmap will be flattened into the parcel if this object is
|
||
|
* sent across processes, so it may end up using a lot of memory, and may be fairly slow.</p>
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setBitmap(@IdRes int viewId, String methodName, Bitmap value) {
|
||
|
addAction(new BitmapReflectionAction(viewId, methodName, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one BlendMode on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setBlendMode(@IdRes int viewId, @NonNull String methodName,
|
||
|
@Nullable BlendMode value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BLEND_MODE, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Bundle on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The value to pass to the method.
|
||
|
*/
|
||
|
public void setBundle(@IdRes int viewId, String methodName, Bundle value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BUNDLE, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Intent on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The {@link android.content.Intent} to pass the method.
|
||
|
*/
|
||
|
public void setIntent(@IdRes int viewId, String methodName, Intent value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INTENT, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Icon on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param value The {@link android.graphics.drawable.Icon} to pass the method.
|
||
|
*/
|
||
|
public void setIcon(@IdRes int viewId, String methodName, Icon value) {
|
||
|
addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.ICON, value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call a method taking one Icon on a view in the layout for this RemoteViews.
|
||
|
*
|
||
|
* @param viewId The id of the view on which to call the method.
|
||
|
* @param methodName The name of the method to call.
|
||
|
* @param notNight The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_NO}
|
||
|
* @param night The value to pass to the method when the view's configuration is set to
|
||
|
* {@link Configuration#UI_MODE_NIGHT_YES}
|
||
|
*/
|
||
|
public void setIcon(
|
||
|
@IdRes int viewId,
|
||
|
@NonNull String methodName,
|
||
|
@Nullable Icon notNight,
|
||
|
@Nullable Icon night) {
|
||
|
addAction(
|
||
|
new NightModeReflectionAction(
|
||
|
viewId,
|
||
|
methodName,
|
||
|
BaseReflectionAction.ICON,
|
||
|
notNight,
|
||
|
night));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling View.setContentDescription(CharSequence).
|
||
|
*
|
||
|
* @param viewId The id of the view whose content description should change.
|
||
|
* @param contentDescription The new content description for the view.
|
||
|
*/
|
||
|
public void setContentDescription(@IdRes int viewId, CharSequence contentDescription) {
|
||
|
setCharSequence(viewId, "setContentDescription", contentDescription);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose before view in accessibility traversal to set.
|
||
|
* @param nextId The id of the next in the accessibility traversal.
|
||
|
**/
|
||
|
public void setAccessibilityTraversalBefore(@IdRes int viewId, @IdRes int nextId) {
|
||
|
setInt(viewId, "setAccessibilityTraversalBefore", nextId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose after view in accessibility traversal to set.
|
||
|
* @param nextId The id of the next in the accessibility traversal.
|
||
|
**/
|
||
|
public void setAccessibilityTraversalAfter(@IdRes int viewId, @IdRes int nextId) {
|
||
|
setInt(viewId, "setAccessibilityTraversalAfter", nextId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link View#setLabelFor(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose property to set.
|
||
|
* @param labeledId The id of a view for which this view serves as a label.
|
||
|
*/
|
||
|
public void setLabelFor(@IdRes int viewId, @IdRes int labeledId) {
|
||
|
setInt(viewId, "setLabelFor", labeledId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.widget.CompoundButton#setChecked(boolean)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose property to set.
|
||
|
* @param checked true to check the button, false to uncheck it.
|
||
|
*/
|
||
|
public void setCompoundButtonChecked(@IdRes int viewId, boolean checked) {
|
||
|
addAction(new SetCompoundButtonCheckedAction(viewId, checked));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Equivalent to calling {@link android.widget.RadioGroup#check(int)}.
|
||
|
*
|
||
|
* @param viewId The id of the view whose property to set.
|
||
|
* @param checkedId The unique id of the radio button to select in the group.
|
||
|
*/
|
||
|
public void setRadioGroupChecked(@IdRes int viewId, @IdRes int checkedId) {
|
||
|
addAction(new SetRadioGroupCheckedAction(viewId, checkedId));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Provides an alternate layout ID, which can be used to inflate this view. This layout will be
|
||
|
* used by the host when the widgets displayed on a light-background where foreground elements
|
||
|
* and text can safely draw using a dark color without any additional background protection.
|
||
|
*/
|
||
|
public void setLightBackgroundLayoutId(@LayoutRes int layoutId) {
|
||
|
mLightBackgroundLayoutId = layoutId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If this view supports dark text versions, creates a copy representing that version,
|
||
|
* otherwise returns itself.
|
||
|
* @hide
|
||
|
*/
|
||
|
public RemoteViews getDarkTextViews() {
|
||
|
if (hasFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT)) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
addFlags(FLAG_USE_LIGHT_BACKGROUND_LAYOUT);
|
||
|
return new RemoteViews(this);
|
||
|
} finally {
|
||
|
mApplyFlags &= ~FLAG_USE_LIGHT_BACKGROUND_LAYOUT;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean hasDrawInstructions() {
|
||
|
return mHasDrawInstructions;
|
||
|
}
|
||
|
|
||
|
private RemoteViews getRemoteViewsToApply(Context context) {
|
||
|
if (hasLandscapeAndPortraitLayouts()) {
|
||
|
int orientation = context.getResources().getConfiguration().orientation;
|
||
|
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||
|
return mLandscape;
|
||
|
}
|
||
|
return mPortrait;
|
||
|
}
|
||
|
if (hasSizedRemoteViews()) {
|
||
|
return findSmallestRemoteView();
|
||
|
}
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the square distance between two points.
|
||
|
*
|
||
|
* This is particularly useful when we only care about the ordering of the distances.
|
||
|
*/
|
||
|
private static float squareDistance(SizeF p1, SizeF p2) {
|
||
|
float dx = p1.getWidth() - p2.getWidth();
|
||
|
float dy = p1.getHeight() - p2.getHeight();
|
||
|
return dx * dx + dy * dy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the layout fits in the space available to the widget.
|
||
|
*
|
||
|
* A layout fits on a widget if the widget size is known (i.e. not null) and both dimensions
|
||
|
* are smaller than the ones of the widget, adding some padding to account for rounding errors.
|
||
|
*/
|
||
|
private static boolean fitsIn(SizeF sizeLayout, @Nullable SizeF sizeWidget) {
|
||
|
return sizeWidget != null && (Math.ceil(sizeWidget.getWidth()) + 1 > sizeLayout.getWidth())
|
||
|
&& (Math.ceil(sizeWidget.getHeight()) + 1 > sizeLayout.getHeight());
|
||
|
}
|
||
|
|
||
|
private RemoteViews findBestFitLayout(@NonNull SizeF widgetSize) {
|
||
|
// Find the better remote view
|
||
|
RemoteViews bestFit = null;
|
||
|
float bestSqDist = Float.MAX_VALUE;
|
||
|
for (RemoteViews layout : mSizedRemoteViews) {
|
||
|
SizeF layoutSize = layout.getIdealSize();
|
||
|
if (layoutSize == null) {
|
||
|
throw new IllegalStateException("Expected RemoteViews to have ideal size");
|
||
|
}
|
||
|
|
||
|
if (fitsIn(layoutSize, widgetSize)) {
|
||
|
if (bestFit == null) {
|
||
|
bestFit = layout;
|
||
|
bestSqDist = squareDistance(layoutSize, widgetSize);
|
||
|
} else {
|
||
|
float newSqDist = squareDistance(layoutSize, widgetSize);
|
||
|
if (newSqDist < bestSqDist) {
|
||
|
bestFit = layout;
|
||
|
bestSqDist = newSqDist;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (bestFit == null) {
|
||
|
Log.w(LOG_TAG, "Could not find a RemoteViews fitting the current size: " + widgetSize);
|
||
|
return findSmallestRemoteView();
|
||
|
}
|
||
|
return bestFit;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the most appropriate {@link RemoteViews} given the context and, if not null, the
|
||
|
* size of the widget.
|
||
|
*
|
||
|
* If {@link RemoteViews#hasSizedRemoteViews()} returns true, the most appropriate view is
|
||
|
* the one that fits in the widget (according to {@link RemoteViews#fitsIn}) and has the
|
||
|
* diagonal the most similar to the widget. If no layout fits or the size of the widget is
|
||
|
* not specified, the one with the smallest area will be chosen.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public RemoteViews getRemoteViewsToApply(@NonNull Context context,
|
||
|
@Nullable SizeF widgetSize) {
|
||
|
if (!hasSizedRemoteViews() || widgetSize == null) {
|
||
|
// If there isn't multiple remote views, fall back on the previous methods.
|
||
|
return getRemoteViewsToApply(context);
|
||
|
}
|
||
|
return findBestFitLayout(widgetSize);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the change of size will lead to using a different {@link RemoteViews}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@Nullable
|
||
|
public RemoteViews getRemoteViewsToApplyIfDifferent(@Nullable SizeF oldSize,
|
||
|
@NonNull SizeF newSize) {
|
||
|
if (!hasSizedRemoteViews()) {
|
||
|
return null;
|
||
|
}
|
||
|
RemoteViews oldBestFit = oldSize == null ? findSmallestRemoteView() : findBestFitLayout(
|
||
|
oldSize);
|
||
|
RemoteViews newBestFit = findBestFitLayout(newSize);
|
||
|
if (oldBestFit != newBestFit) {
|
||
|
return newBestFit;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Inflates the view hierarchy represented by this object and applies
|
||
|
* all of the actions.
|
||
|
*
|
||
|
* <p><strong>Caller beware: this may throw</strong>
|
||
|
*
|
||
|
* @param context Default context to use
|
||
|
* @param parent Parent that the resulting view hierarchy will be attached to. This method
|
||
|
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
|
||
|
* @return The inflated view hierarchy
|
||
|
*/
|
||
|
public View apply(Context context, ViewGroup parent) {
|
||
|
return apply(context, parent, null);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public View apply(Context context, ViewGroup parent, InteractionHandler handler) {
|
||
|
return apply(context, parent, handler, null);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public View apply(@NonNull Context context, @NonNull ViewGroup parent,
|
||
|
@Nullable InteractionHandler handler, @Nullable SizeF size) {
|
||
|
return apply(context, parent, size, new ActionApplyParams()
|
||
|
.withInteractionHandler(handler));
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public View applyWithTheme(@NonNull Context context, @NonNull ViewGroup parent,
|
||
|
@Nullable InteractionHandler handler, @StyleRes int applyThemeResId) {
|
||
|
return apply(context, parent, null, new ActionApplyParams()
|
||
|
.withInteractionHandler(handler)
|
||
|
.withThemeResId(applyThemeResId));
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public View apply(Context context, ViewGroup parent, InteractionHandler handler,
|
||
|
@Nullable SizeF size, @Nullable ColorResources colorResources) {
|
||
|
return apply(context, parent, size, new ActionApplyParams()
|
||
|
.withInteractionHandler(handler)
|
||
|
.withColorResources(colorResources));
|
||
|
}
|
||
|
|
||
|
/** @hide **/
|
||
|
public View apply(Context context, ViewGroup parent, @Nullable SizeF size,
|
||
|
ActionApplyParams params) {
|
||
|
return apply(context, parent, parent, size, params);
|
||
|
}
|
||
|
|
||
|
private View apply(Context context, ViewGroup directParent, ViewGroup rootParent,
|
||
|
@Nullable SizeF size, ActionApplyParams params) {
|
||
|
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
|
||
|
View result = inflateView(context, rvToApply, directParent,
|
||
|
params.applyThemeResId, params.colorResources);
|
||
|
rvToApply.performApply(result, rootParent, params);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
private View inflateView(Context context, RemoteViews rv, @Nullable ViewGroup parent,
|
||
|
@StyleRes int applyThemeResId, @Nullable ColorResources colorResources) {
|
||
|
// RemoteViews may be built by an application installed in another
|
||
|
// user. So build a context that loads resources from that user but
|
||
|
// still returns the current users userId so settings like data / time formats
|
||
|
// are loaded without requiring cross user persmissions.
|
||
|
final Context contextForResources =
|
||
|
getContextForResourcesEnsuringCorrectCachedApkPaths(context);
|
||
|
if (colorResources != null) {
|
||
|
colorResources.apply(contextForResources);
|
||
|
}
|
||
|
Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);
|
||
|
|
||
|
// If mApplyThemeResId is not given, Theme.DeviceDefault will be used.
|
||
|
if (applyThemeResId != 0) {
|
||
|
inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId);
|
||
|
}
|
||
|
View v;
|
||
|
// If the RemoteViews contains draw instructions, just use it instead.
|
||
|
if (rv.hasDrawInstructions()) {
|
||
|
final RemoteComposePlayer player = new RemoteComposePlayer(inflationContext);
|
||
|
player.setDebug(Build.IS_USERDEBUG || Build.IS_ENG ? 1 : 0);
|
||
|
v = player;
|
||
|
} else {
|
||
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||
|
|
||
|
// Clone inflater so we load resources from correct context and
|
||
|
// we don't add a filter to the static version returned by getSystemService.
|
||
|
inflater = inflater.cloneInContext(inflationContext);
|
||
|
inflater.setFilter(shouldUseStaticFilter() ? INFLATER_FILTER : this);
|
||
|
if (mLayoutInflaterFactory2 != null) {
|
||
|
inflater.setFactory2(mLayoutInflaterFactory2);
|
||
|
}
|
||
|
v = inflater.inflate(rv.getLayoutId(), parent, false);
|
||
|
}
|
||
|
if (mViewId != View.NO_ID) {
|
||
|
v.setId(mViewId);
|
||
|
v.setTagInternal(R.id.remote_views_override_id, mViewId);
|
||
|
}
|
||
|
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
|
||
|
return v;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A static filter is much lighter than RemoteViews itself. It's optimized here only for
|
||
|
* RemoteVies class. Subclasses should always override this and return true if not overriding
|
||
|
* {@link this#onLoadClass(Class)}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
protected boolean shouldUseStaticFilter() {
|
||
|
return this.getClass().equals(RemoteViews.class);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Implement this interface to receive a callback when
|
||
|
* {@link #applyAsync} or {@link #reapplyAsync} is finished.
|
||
|
* @hide
|
||
|
*/
|
||
|
public interface OnViewAppliedListener {
|
||
|
/**
|
||
|
* Callback when the RemoteView has finished inflating,
|
||
|
* but no actions have been applied yet.
|
||
|
*/
|
||
|
default void onViewInflated(View v) {}
|
||
|
|
||
|
void onViewApplied(View v);
|
||
|
|
||
|
void onError(Exception e);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies the views asynchronously, moving as much of the task on the background
|
||
|
* thread as possible.
|
||
|
*
|
||
|
* @see #apply(Context, ViewGroup)
|
||
|
* @param context Default context to use
|
||
|
* @param parent Parent that the resulting view hierarchy will be attached to. This method
|
||
|
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
|
||
|
* @param listener the callback to run when all actions have been applied. May be null.
|
||
|
* @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used.
|
||
|
* @return CancellationSignal
|
||
|
* @hide
|
||
|
*/
|
||
|
public CancellationSignal applyAsync(
|
||
|
Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) {
|
||
|
return applyAsync(context, parent, executor, listener, null /* handler */);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public CancellationSignal applyAsync(Context context, ViewGroup parent,
|
||
|
Executor executor, OnViewAppliedListener listener, InteractionHandler handler) {
|
||
|
return applyAsync(context, parent, executor, listener, handler, null /* size */);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public CancellationSignal applyAsync(Context context, ViewGroup parent,
|
||
|
Executor executor, OnViewAppliedListener listener, InteractionHandler handler,
|
||
|
SizeF size) {
|
||
|
return applyAsync(context, parent, executor, listener, handler, size,
|
||
|
null /* themeColors */);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor,
|
||
|
OnViewAppliedListener listener, InteractionHandler handler, SizeF size,
|
||
|
ColorResources colorResources) {
|
||
|
|
||
|
ActionApplyParams params = new ActionApplyParams()
|
||
|
.withInteractionHandler(handler)
|
||
|
.withColorResources(colorResources)
|
||
|
.withExecutor(executor);
|
||
|
return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener,
|
||
|
params, null /* result */, true /* topLevel */).startTaskOnExecutor(executor);
|
||
|
}
|
||
|
|
||
|
private AsyncApplyTask getInternalAsyncApplyTask(Context context, ViewGroup parent,
|
||
|
OnViewAppliedListener listener, ActionApplyParams params, SizeF size, View result) {
|
||
|
return new AsyncApplyTask(getRemoteViewsToApply(context, size), parent, context, listener,
|
||
|
params, result, false /* topLevel */);
|
||
|
}
|
||
|
|
||
|
private class AsyncApplyTask extends AsyncTask<Void, Void, ViewTree>
|
||
|
implements CancellationSignal.OnCancelListener {
|
||
|
final CancellationSignal mCancelSignal = new CancellationSignal();
|
||
|
final RemoteViews mRV;
|
||
|
final ViewGroup mParent;
|
||
|
final Context mContext;
|
||
|
final OnViewAppliedListener mListener;
|
||
|
final ActionApplyParams mApplyParams;
|
||
|
|
||
|
/**
|
||
|
* Whether the remote view is the top-level one (i.e. not within an action).
|
||
|
*
|
||
|
* This is only used if the result is specified (i.e. the view is being recycled).
|
||
|
*/
|
||
|
final boolean mTopLevel;
|
||
|
|
||
|
private View mResult;
|
||
|
private ViewTree mTree;
|
||
|
private Action[] mActions;
|
||
|
private Exception mError;
|
||
|
|
||
|
private AsyncApplyTask(
|
||
|
RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener,
|
||
|
ActionApplyParams applyParams, View result, boolean topLevel) {
|
||
|
mRV = rv;
|
||
|
mParent = parent;
|
||
|
mContext = context;
|
||
|
mListener = listener;
|
||
|
mTopLevel = topLevel;
|
||
|
mApplyParams = applyParams;
|
||
|
mResult = result;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
@Override
|
||
|
protected ViewTree doInBackground(Void... params) {
|
||
|
try {
|
||
|
if (mResult == null) {
|
||
|
mResult = inflateView(mContext, mRV, mParent, 0, mApplyParams.colorResources);
|
||
|
}
|
||
|
|
||
|
mTree = new ViewTree(mResult);
|
||
|
|
||
|
if (mRV.mActions != null) {
|
||
|
int count = mRV.mActions.size();
|
||
|
mActions = new Action[count];
|
||
|
for (int i = 0; i < count && !isCancelled(); i++) {
|
||
|
// TODO: check if isCancelled in nested views.
|
||
|
mActions[i] = mRV.mActions.get(i)
|
||
|
.initActionAsync(mTree, mParent, mApplyParams);
|
||
|
}
|
||
|
} else {
|
||
|
mActions = null;
|
||
|
}
|
||
|
return mTree;
|
||
|
} catch (Exception e) {
|
||
|
mError = e;
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onPostExecute(ViewTree viewTree) {
|
||
|
mCancelSignal.setOnCancelListener(null);
|
||
|
if (mError == null) {
|
||
|
if (mListener != null) {
|
||
|
mListener.onViewInflated(viewTree.mRoot);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
if (mActions != null) {
|
||
|
|
||
|
ActionApplyParams applyParams = mApplyParams.clone();
|
||
|
if (applyParams.handler == null) {
|
||
|
applyParams.handler = DEFAULT_INTERACTION_HANDLER;
|
||
|
}
|
||
|
for (Action a : mActions) {
|
||
|
a.apply(viewTree.mRoot, mParent, applyParams);
|
||
|
}
|
||
|
}
|
||
|
// If the parent of the view is has is a root, resolve the recycling.
|
||
|
if (mTopLevel && mResult instanceof ViewGroup) {
|
||
|
finalizeViewRecycling((ViewGroup) mResult);
|
||
|
}
|
||
|
} catch (Exception e) {
|
||
|
mError = e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (mListener != null) {
|
||
|
if (mError != null) {
|
||
|
mListener.onError(mError);
|
||
|
} else {
|
||
|
mListener.onViewApplied(viewTree.mRoot);
|
||
|
}
|
||
|
} else if (mError != null) {
|
||
|
if (mError instanceof ActionException) {
|
||
|
throw (ActionException) mError;
|
||
|
} else {
|
||
|
throw new ActionException(mError);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onCancel() {
|
||
|
cancel(true);
|
||
|
}
|
||
|
|
||
|
private CancellationSignal startTaskOnExecutor(Executor executor) {
|
||
|
mCancelSignal.setOnCancelListener(this);
|
||
|
executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor);
|
||
|
return mCancelSignal;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies all of the actions to the provided view.
|
||
|
*
|
||
|
* <p><strong>Caller beware: this may throw</strong>
|
||
|
*
|
||
|
* @param v The view to apply the actions to. This should be the result of
|
||
|
* the {@link #apply(Context,ViewGroup)} call.
|
||
|
*/
|
||
|
public void reapply(Context context, View v) {
|
||
|
reapply(context, v, null /* size */, new ActionApplyParams());
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public void reapply(Context context, View v, InteractionHandler handler) {
|
||
|
reapply(context, v, null /* size */,
|
||
|
new ActionApplyParams().withInteractionHandler(handler));
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public void reapply(Context context, View v, InteractionHandler handler, SizeF size,
|
||
|
ColorResources colorResources) {
|
||
|
reapply(context, v, size, new ActionApplyParams()
|
||
|
.withInteractionHandler(handler).withColorResources(colorResources));
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public void reapply(Context context, View v, @Nullable SizeF size, ActionApplyParams params) {
|
||
|
reapply(context, v, (ViewGroup) v.getParent(), size, params, true);
|
||
|
}
|
||
|
|
||
|
private void reapplyNestedViews(Context context, View v, ViewGroup rootParent,
|
||
|
ActionApplyParams params) {
|
||
|
reapply(context, v, rootParent, null, params, false);
|
||
|
}
|
||
|
|
||
|
// Note: topLevel should be true only for calls on the topLevel RemoteViews, internal calls
|
||
|
// should set it to false.
|
||
|
private void reapply(Context context, View v, ViewGroup rootParent,
|
||
|
@Nullable SizeF size, ActionApplyParams params, boolean topLevel) {
|
||
|
RemoteViews rvToApply = getRemoteViewsToReapply(context, v, size);
|
||
|
rvToApply.performApply(v, rootParent, params);
|
||
|
|
||
|
// If the parent of the view is has is a root, resolve the recycling.
|
||
|
if (topLevel && v instanceof ViewGroup) {
|
||
|
finalizeViewRecycling((ViewGroup) v);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public boolean canRecycleView(@Nullable View v) {
|
||
|
if (v == null || hasDrawInstructions()) {
|
||
|
return false;
|
||
|
}
|
||
|
Integer previousLayoutId = (Integer) v.getTag(R.id.widget_frame);
|
||
|
if (previousLayoutId == null) {
|
||
|
return false;
|
||
|
}
|
||
|
Integer overrideIdTag = (Integer) v.getTag(R.id.remote_views_override_id);
|
||
|
int overrideId = overrideIdTag == null ? View.NO_ID : overrideIdTag;
|
||
|
// If mViewId is View.NO_ID, we only recycle if overrideId is also View.NO_ID.
|
||
|
// Otherwise, it might be that, on a previous iteration, the view's ID was set to
|
||
|
// something else, and it should now be reset to the ID defined in the XML layout file,
|
||
|
// whatever it is.
|
||
|
return previousLayoutId == getLayoutId() && mViewId == overrideId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the RemoteViews that should be used in the reapply operation.
|
||
|
*
|
||
|
* If the current RemoteViews has multiple layout, this will select the correct one.
|
||
|
*
|
||
|
* @throws RuntimeException If the current RemoteViews should not be reapplied onto the provided
|
||
|
* View.
|
||
|
*/
|
||
|
private RemoteViews getRemoteViewsToReapply(Context context, View v, @Nullable SizeF size) {
|
||
|
RemoteViews rvToApply = getRemoteViewsToApply(context, size);
|
||
|
|
||
|
// In the case that a view has this RemoteViews applied in one orientation or size, is
|
||
|
// persisted across change, and has the RemoteViews re-applied in a different situation
|
||
|
// (orientation or size), we throw an exception, since the layouts may be completely
|
||
|
// unrelated.
|
||
|
// If the ViewID has been changed on the view, or is changed by the RemoteViews, we also
|
||
|
// may throw an exception, as the RemoteViews will probably not apply properly.
|
||
|
// However, we need to let potentially unrelated RemoteViews apply, as this lack of testing
|
||
|
// is already used in production code in some apps.
|
||
|
if (hasMultipleLayouts()
|
||
|
|| rvToApply.mViewId != View.NO_ID
|
||
|
|| v.getTag(R.id.remote_views_override_id) != null) {
|
||
|
if (!rvToApply.canRecycleView(v)) {
|
||
|
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
|
||
|
" that does not share the same root layout id.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return rvToApply;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies all the actions to the provided view, moving as much of the task on the background
|
||
|
* thread as possible.
|
||
|
*
|
||
|
* @see #reapply(Context, View)
|
||
|
* @param context Default context to use
|
||
|
* @param v The view to apply the actions to. This should be the result of
|
||
|
* the {@link #apply(Context,ViewGroup)} call.
|
||
|
* @param listener the callback to run when all actions have been applied. May be null.
|
||
|
* @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used
|
||
|
* @return CancellationSignal
|
||
|
* @hide
|
||
|
*/
|
||
|
public CancellationSignal reapplyAsync(Context context, View v, Executor executor,
|
||
|
OnViewAppliedListener listener) {
|
||
|
return reapplyAsync(context, v, executor, listener, null);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public CancellationSignal reapplyAsync(Context context, View v, Executor executor,
|
||
|
OnViewAppliedListener listener, InteractionHandler handler) {
|
||
|
return reapplyAsync(context, v, executor, listener, handler, null, null);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public CancellationSignal reapplyAsync(Context context, View v, Executor executor,
|
||
|
OnViewAppliedListener listener, InteractionHandler handler, SizeF size,
|
||
|
ColorResources colorResources) {
|
||
|
RemoteViews rvToApply = getRemoteViewsToReapply(context, v, size);
|
||
|
|
||
|
ActionApplyParams params = new ActionApplyParams()
|
||
|
.withColorResources(colorResources)
|
||
|
.withInteractionHandler(handler)
|
||
|
.withExecutor(executor);
|
||
|
|
||
|
return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
|
||
|
context, listener, params, v, true /* topLevel */)
|
||
|
.startTaskOnExecutor(executor);
|
||
|
}
|
||
|
|
||
|
private void performApply(View v, ViewGroup parent, ActionApplyParams params) {
|
||
|
params = params.clone();
|
||
|
if (params.handler == null) {
|
||
|
params.handler = DEFAULT_INTERACTION_HANDLER;
|
||
|
}
|
||
|
if (v instanceof RemoteComposePlayer player) {
|
||
|
player.setTheme(v.getResources().getConfiguration().isNightModeActive()
|
||
|
? Theme.DARK : Theme.LIGHT);
|
||
|
}
|
||
|
if (mActions != null) {
|
||
|
final int count = mActions.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
mActions.get(i).apply(v, parent, params);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the RemoteViews contains potentially costly operations and should be
|
||
|
* applied asynchronously.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean prefersAsyncApply() {
|
||
|
if (mActions != null) {
|
||
|
final int count = mActions.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
if (mActions.get(i).prefersAsyncApply()) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public void updateAppInfo(@NonNull ApplicationInfo info) {
|
||
|
ApplicationInfo existing = mApplicationInfoCache.get(info);
|
||
|
if (existing != null && !existing.sourceDir.equals(info.sourceDir)) {
|
||
|
// Overlay paths are generated against a particular version of an application.
|
||
|
// The overlays paths of a newly upgraded application are incompatible with the
|
||
|
// old version of the application.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If we can update to the new AppInfo, put it in the cache and propagate the change
|
||
|
// throughout the hierarchy.
|
||
|
mApplicationInfoCache.put(info);
|
||
|
configureDescendantsAsChildren();
|
||
|
}
|
||
|
|
||
|
private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) {
|
||
|
if (mApplication != null) {
|
||
|
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
|
||
|
&& context.getPackageName().equals(mApplication.packageName)) {
|
||
|
return context;
|
||
|
}
|
||
|
try {
|
||
|
LoadedApk.checkAndUpdateApkPaths(mApplication);
|
||
|
return context.createApplicationContext(mApplication,
|
||
|
Context.CONTEXT_RESTRICTED);
|
||
|
} catch (NameNotFoundException e) {
|
||
|
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return context;
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private SparseArray<PendingIntent> getPendingIntentTemplate() {
|
||
|
if (mPendingIntentTemplate == null) {
|
||
|
mPendingIntentTemplate = new SparseArray<>();
|
||
|
}
|
||
|
return mPendingIntentTemplate;
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private SparseArray<Intent> getFillInIntent() {
|
||
|
if (mFillInIntent == null) {
|
||
|
mFillInIntent = new SparseArray<>();
|
||
|
}
|
||
|
return mFillInIntent;
|
||
|
}
|
||
|
|
||
|
private void tryAddRemoteResponse(final int viewId) {
|
||
|
final PendingIntent pendingIntent = getPendingIntentTemplate().get(viewId);
|
||
|
final Intent intent = getFillInIntent().get(viewId);
|
||
|
if (pendingIntent != null && intent != null) {
|
||
|
addAction(new SetOnClickResponse(viewId,
|
||
|
RemoteResponse.fromPendingIntentTemplateAndFillInIntent(
|
||
|
pendingIntent, intent)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Utility class to hold all the options when applying the remote views
|
||
|
* @hide
|
||
|
*/
|
||
|
public class ActionApplyParams {
|
||
|
public InteractionHandler handler;
|
||
|
public ColorResources colorResources;
|
||
|
public Executor executor;
|
||
|
@StyleRes public int applyThemeResId;
|
||
|
|
||
|
@Override
|
||
|
public ActionApplyParams clone() {
|
||
|
return new ActionApplyParams()
|
||
|
.withInteractionHandler(handler)
|
||
|
.withColorResources(colorResources)
|
||
|
.withExecutor(executor)
|
||
|
.withThemeResId(applyThemeResId);
|
||
|
}
|
||
|
|
||
|
public ActionApplyParams withInteractionHandler(InteractionHandler handler) {
|
||
|
this.handler = handler;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public ActionApplyParams withColorResources(ColorResources colorResources) {
|
||
|
this.colorResources = colorResources;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public ActionApplyParams withThemeResId(@StyleRes int themeResId) {
|
||
|
this.applyThemeResId = themeResId;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public ActionApplyParams withExecutor(Executor executor) {
|
||
|
this.executor = executor;
|
||
|
return this;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Object allowing the modification of a context to overload the system's dynamic colors.
|
||
|
*
|
||
|
* Only colors from {@link android.R.color#system_accent1_0} to
|
||
|
* {@link android.R.color#system_neutral2_1000} can be overloaded.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final class ColorResources {
|
||
|
// Set of valid colors resources.
|
||
|
private static final int FIRST_RESOURCE_COLOR_ID = android.R.color.system_neutral1_0;
|
||
|
private static final int LAST_RESOURCE_COLOR_ID = android.R.color.system_accent3_1000;
|
||
|
// Size, in bytes, of an entry in the array of colors in an ARSC file.
|
||
|
private static final int ARSC_ENTRY_SIZE = 16;
|
||
|
|
||
|
private final ResourcesLoader mLoader;
|
||
|
private final SparseIntArray mColorMapping;
|
||
|
|
||
|
private ColorResources(ResourcesLoader loader, SparseIntArray colorMapping) {
|
||
|
mLoader = loader;
|
||
|
mColorMapping = colorMapping;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply the color resources to the given context.
|
||
|
*
|
||
|
* No resource resolution must have be done on the context given to that method.
|
||
|
*/
|
||
|
public void apply(Context context) {
|
||
|
context.getResources().addLoaders(mLoader);
|
||
|
}
|
||
|
|
||
|
public SparseIntArray getColorMapping() {
|
||
|
return mColorMapping;
|
||
|
}
|
||
|
|
||
|
private static ByteArrayOutputStream readFileContent(InputStream input) throws IOException {
|
||
|
ByteArrayOutputStream content = new ByteArrayOutputStream(2048);
|
||
|
byte[] buffer = new byte[4096];
|
||
|
while (input.available() > 0) {
|
||
|
int read = input.read(buffer);
|
||
|
content.write(buffer, 0, read);
|
||
|
}
|
||
|
return content;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates the compiled resources content from the asset stored in the APK.
|
||
|
*
|
||
|
* The asset is a compiled resource with the correct resources name and correct ids, only
|
||
|
* the values are incorrect. The last value is at the very end of the file. The resources
|
||
|
* are in an array, the array's entries are 16 bytes each. We use this to work out the
|
||
|
* location of all the positions of the various resources.
|
||
|
*/
|
||
|
@Nullable
|
||
|
private static byte[] createCompiledResourcesContent(Context context,
|
||
|
SparseIntArray colorResources) throws IOException {
|
||
|
byte[] content;
|
||
|
try (InputStream input = context.getResources().openRawResource(
|
||
|
com.android.internal.R.raw.remote_views_color_resources)) {
|
||
|
ByteArrayOutputStream rawContent = readFileContent(input);
|
||
|
content = rawContent.toByteArray();
|
||
|
}
|
||
|
int valuesOffset =
|
||
|
content.length - (LAST_RESOURCE_COLOR_ID & 0xffff) * ARSC_ENTRY_SIZE - 4;
|
||
|
if (valuesOffset < 0) {
|
||
|
Log.e(LOG_TAG, "ARSC file for theme colors is invalid.");
|
||
|
return null;
|
||
|
}
|
||
|
for (int colorRes = FIRST_RESOURCE_COLOR_ID; colorRes <= LAST_RESOURCE_COLOR_ID;
|
||
|
colorRes++) {
|
||
|
// The last 2 bytes are the index in the color array.
|
||
|
int index = colorRes & 0xffff;
|
||
|
int offset = valuesOffset + index * ARSC_ENTRY_SIZE;
|
||
|
int value = colorResources.get(colorRes, context.getColor(colorRes));
|
||
|
// Write the 32 bit integer in little endian
|
||
|
for (int b = 0; b < 4; b++) {
|
||
|
content[offset + b] = (byte) (value & 0xff);
|
||
|
value >>= 8;
|
||
|
}
|
||
|
}
|
||
|
return content;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a resource loader for theme colors to the given context.
|
||
|
*
|
||
|
* @param context Context of the view hosting the widget.
|
||
|
* @param colorMapping Mapping of resources to color values.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@Nullable
|
||
|
public static ColorResources create(Context context, SparseIntArray colorMapping) {
|
||
|
try {
|
||
|
byte[] contentBytes = createCompiledResourcesContent(context, colorMapping);
|
||
|
if (contentBytes == null) {
|
||
|
return null;
|
||
|
}
|
||
|
FileDescriptor arscFile = null;
|
||
|
try {
|
||
|
arscFile = Os.memfd_create("remote_views_theme_colors.arsc", 0 /* flags */);
|
||
|
// Note: This must not be closed through the OutputStream.
|
||
|
try (OutputStream pipeWriter = new FileOutputStream(arscFile)) {
|
||
|
pipeWriter.write(contentBytes);
|
||
|
|
||
|
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(arscFile)) {
|
||
|
ResourcesLoader colorsLoader = new ResourcesLoader();
|
||
|
colorsLoader.addProvider(ResourcesProvider
|
||
|
.loadFromTable(pfd, null /* assetsProvider */));
|
||
|
return new ColorResources(colorsLoader, colorMapping.clone());
|
||
|
}
|
||
|
}
|
||
|
} finally {
|
||
|
if (arscFile != null) {
|
||
|
Os.close(arscFile);
|
||
|
}
|
||
|
}
|
||
|
} catch (Exception ex) {
|
||
|
Log.e(LOG_TAG, "Failed to setup the context for theme colors", ex);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the number of actions in this RemoteViews. Can be used as a sequence number.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public int getSequenceNumber() {
|
||
|
return (mActions == null) ? 0 : mActions.size();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used to restrict the views which can be inflated
|
||
|
*
|
||
|
* @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class)
|
||
|
* @deprecated Used by system to enforce safe inflation of {@link RemoteViews}. Apps should not
|
||
|
* override this method. Changing of this method will NOT affect the process where RemoteViews
|
||
|
* is rendered.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public boolean onLoadClass(Class clazz) {
|
||
|
return clazz.isAnnotationPresent(RemoteView.class);
|
||
|
}
|
||
|
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel dest, int flags) {
|
||
|
writeToParcel(dest, flags, /* intentsToIgnore= */ null);
|
||
|
}
|
||
|
|
||
|
private void writeToParcel(Parcel dest, int flags,
|
||
|
@Nullable SparseArray<Intent> intentsToIgnore) {
|
||
|
boolean prevSquashingAllowed = dest.allowSquashing();
|
||
|
|
||
|
if (!hasMultipleLayouts()) {
|
||
|
dest.writeInt(MODE_NORMAL);
|
||
|
// We only write the bitmap cache if we are the root RemoteViews, as this cache
|
||
|
// is shared by all children.
|
||
|
if (mIsRoot) {
|
||
|
mBitmapCache.writeBitmapsToParcel(dest, flags);
|
||
|
mCollectionCache.writeToParcel(dest, flags, intentsToIgnore);
|
||
|
}
|
||
|
dest.writeTypedObject(mApplication, flags);
|
||
|
if (mIsRoot || mIdealSize == null) {
|
||
|
dest.writeInt(0);
|
||
|
} else {
|
||
|
dest.writeInt(1);
|
||
|
mIdealSize.writeToParcel(dest, flags);
|
||
|
}
|
||
|
dest.writeInt(mLayoutId);
|
||
|
dest.writeInt(mViewId);
|
||
|
dest.writeInt(mLightBackgroundLayoutId);
|
||
|
writeActionsToParcel(dest, flags);
|
||
|
} else if (hasSizedRemoteViews()) {
|
||
|
dest.writeInt(MODE_HAS_SIZED_REMOTEVIEWS);
|
||
|
if (mIsRoot) {
|
||
|
mBitmapCache.writeBitmapsToParcel(dest, flags);
|
||
|
mCollectionCache.writeToParcel(dest, flags, intentsToIgnore);
|
||
|
}
|
||
|
dest.writeInt(mSizedRemoteViews.size());
|
||
|
for (RemoteViews view : mSizedRemoteViews) {
|
||
|
view.writeToParcel(dest, flags);
|
||
|
}
|
||
|
} else {
|
||
|
dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT);
|
||
|
// We only write the bitmap cache if we are the root RemoteViews, as this cache
|
||
|
// is shared by all children.
|
||
|
if (mIsRoot) {
|
||
|
mBitmapCache.writeBitmapsToParcel(dest, flags);
|
||
|
mCollectionCache.writeToParcel(dest, flags, intentsToIgnore);
|
||
|
}
|
||
|
mLandscape.writeToParcel(dest, flags);
|
||
|
// Both RemoteViews already share the same package and user
|
||
|
mPortrait.writeToParcel(dest, flags);
|
||
|
}
|
||
|
dest.writeInt(mApplyFlags);
|
||
|
dest.writeLong(mProviderInstanceId);
|
||
|
dest.writeBoolean(mHasDrawInstructions);
|
||
|
|
||
|
dest.restoreAllowSquashing(prevSquashingAllowed);
|
||
|
}
|
||
|
|
||
|
private void writeActionsToParcel(Parcel parcel, int flags) {
|
||
|
int count;
|
||
|
if (mActions != null) {
|
||
|
count = mActions.size();
|
||
|
} else {
|
||
|
count = 0;
|
||
|
}
|
||
|
parcel.writeInt(count);
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
Action a = mActions.get(i);
|
||
|
parcel.writeInt(a.getActionTag());
|
||
|
a.writeToParcel(parcel, flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static ApplicationInfo getApplicationInfo(@Nullable String packageName, int userId) {
|
||
|
if (packageName == null) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Get the application for the passed in package and user.
|
||
|
Application application = ActivityThread.currentApplication();
|
||
|
if (application == null) {
|
||
|
throw new IllegalStateException("Cannot create remote views out of an aplication.");
|
||
|
}
|
||
|
|
||
|
ApplicationInfo applicationInfo = application.getApplicationInfo();
|
||
|
if (UserHandle.getUserId(applicationInfo.uid) != userId
|
||
|
|| !applicationInfo.packageName.equals(packageName)) {
|
||
|
try {
|
||
|
Context context = application.getBaseContext().createPackageContextAsUser(
|
||
|
packageName, 0, new UserHandle(userId));
|
||
|
applicationInfo = context.getApplicationInfo();
|
||
|
} catch (NameNotFoundException nnfe) {
|
||
|
throw new IllegalArgumentException("No such package " + packageName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return applicationInfo;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the {@link #mApplication} is same as the provided info.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean hasSameAppInfo(ApplicationInfo info) {
|
||
|
return mApplication == null || mApplication.packageName.equals(info.packageName)
|
||
|
&& mApplication.uid == info.uid;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parcelable.Creator that instantiates RemoteViews objects
|
||
|
*/
|
||
|
@NonNull
|
||
|
public static final Parcelable.Creator<RemoteViews> CREATOR =
|
||
|
new Parcelable.Creator<RemoteViews>() {
|
||
|
public RemoteViews createFromParcel(Parcel parcel) {
|
||
|
return new RemoteViews(parcel);
|
||
|
}
|
||
|
|
||
|
public RemoteViews[] newArray(int size) {
|
||
|
return new RemoteViews[size];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A representation of the view hierarchy. Only views which have a valid ID are added
|
||
|
* and can be searched.
|
||
|
*/
|
||
|
private static class ViewTree {
|
||
|
private static final int INSERT_AT_END_INDEX = -1;
|
||
|
private View mRoot;
|
||
|
private ArrayList<ViewTree> mChildren;
|
||
|
|
||
|
private ViewTree(View root) {
|
||
|
mRoot = root;
|
||
|
}
|
||
|
|
||
|
public void createTree() {
|
||
|
if (mChildren != null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mChildren = new ArrayList<>();
|
||
|
if (mRoot instanceof ViewGroup) {
|
||
|
ViewGroup vg = (ViewGroup) mRoot;
|
||
|
int count = vg.getChildCount();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
addViewChild(vg.getChildAt(i));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
public ViewTree findViewTreeById(@IdRes int id) {
|
||
|
if (mRoot.getId() == id) {
|
||
|
return this;
|
||
|
}
|
||
|
if (mChildren == null) {
|
||
|
return null;
|
||
|
}
|
||
|
for (ViewTree tree : mChildren) {
|
||
|
ViewTree result = tree.findViewTreeById(id);
|
||
|
if (result != null) {
|
||
|
return result;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
public ViewTree findViewTreeParentOf(ViewTree child) {
|
||
|
if (mChildren == null) {
|
||
|
return null;
|
||
|
}
|
||
|
for (ViewTree tree : mChildren) {
|
||
|
if (tree == child) {
|
||
|
return this;
|
||
|
}
|
||
|
ViewTree result = tree.findViewTreeParentOf(child);
|
||
|
if (result != null) {
|
||
|
return result;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public void replaceView(View v) {
|
||
|
mRoot = v;
|
||
|
mChildren = null;
|
||
|
createTree();
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
public <T extends View> T findViewById(@IdRes int id) {
|
||
|
if (mChildren == null) {
|
||
|
return mRoot.findViewById(id);
|
||
|
}
|
||
|
ViewTree tree = findViewTreeById(id);
|
||
|
return tree == null ? null : (T) tree.mRoot;
|
||
|
}
|
||
|
|
||
|
public void addChild(ViewTree child) {
|
||
|
addChild(child, INSERT_AT_END_INDEX);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds the given {@link ViewTree} as a child at the given index.
|
||
|
*
|
||
|
* @param index The position at which to add the child or -1 to add last.
|
||
|
*/
|
||
|
public void addChild(ViewTree child, int index) {
|
||
|
if (mChildren == null) {
|
||
|
mChildren = new ArrayList<>();
|
||
|
}
|
||
|
child.createTree();
|
||
|
|
||
|
if (index == INSERT_AT_END_INDEX) {
|
||
|
mChildren.add(child);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mChildren.add(index, child);
|
||
|
}
|
||
|
|
||
|
public void removeChildren(int start, int count) {
|
||
|
if (mChildren != null) {
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
mChildren.remove(start);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void addViewChild(View v) {
|
||
|
// ViewTree only contains Views which can be found using findViewById.
|
||
|
// If isRootNamespace is true, this view is skipped.
|
||
|
// @see ViewGroup#findViewTraversal(int)
|
||
|
if (v.isRootNamespace()) {
|
||
|
return;
|
||
|
}
|
||
|
final ViewTree target;
|
||
|
|
||
|
// If the view has a valid id, i.e., if can be found using findViewById, add it to the
|
||
|
// tree, otherwise skip this view and add its children instead.
|
||
|
if (v.getId() != 0) {
|
||
|
ViewTree tree = new ViewTree(v);
|
||
|
mChildren.add(tree);
|
||
|
target = tree;
|
||
|
} else {
|
||
|
target = this;
|
||
|
}
|
||
|
|
||
|
if (v instanceof ViewGroup) {
|
||
|
if (target.mChildren == null) {
|
||
|
target.mChildren = new ArrayList<>();
|
||
|
ViewGroup vg = (ViewGroup) v;
|
||
|
int count = vg.getChildCount();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
target.addViewChild(vg.getChildAt(i));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Find the first child for which the condition is true and return its index. */
|
||
|
public int findChildIndex(Predicate<View> condition) {
|
||
|
return findChildIndex(0, condition);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Find the first child, starting at {@code startIndex}, for which the condition is true and
|
||
|
* return its index.
|
||
|
*/
|
||
|
public int findChildIndex(int startIndex, Predicate<View> condition) {
|
||
|
if (mChildren == null) {
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
for (int i = startIndex; i < mChildren.size(); i++) {
|
||
|
if (condition.test(mChildren.get(i).mRoot)) {
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Class representing a response to an action performed on any element of a RemoteViews.
|
||
|
*/
|
||
|
public static class RemoteResponse {
|
||
|
|
||
|
/** @hide **/
|
||
|
@IntDef(prefix = "INTERACTION_TYPE_", value = {
|
||
|
INTERACTION_TYPE_CLICK,
|
||
|
INTERACTION_TYPE_CHECKED_CHANGE,
|
||
|
})
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
@interface InteractionType {}
|
||
|
/** @hide */
|
||
|
public static final int INTERACTION_TYPE_CLICK = 0;
|
||
|
/** @hide */
|
||
|
public static final int INTERACTION_TYPE_CHECKED_CHANGE = 1;
|
||
|
|
||
|
private PendingIntent mPendingIntent;
|
||
|
private Intent mFillIntent;
|
||
|
|
||
|
private int mInteractionType = INTERACTION_TYPE_CLICK;
|
||
|
private IntArray mViewIds;
|
||
|
private ArrayList<String> mElementNames;
|
||
|
|
||
|
/**
|
||
|
* Creates a response which sends a pending intent as part of the response. The source
|
||
|
* bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the
|
||
|
* target view in screen space.
|
||
|
* Note that any activity options associated with the mPendingIntent may get overridden
|
||
|
* before starting the intent.
|
||
|
*
|
||
|
* @param pendingIntent The {@link PendingIntent} to send as part of the response
|
||
|
*/
|
||
|
@NonNull
|
||
|
public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) {
|
||
|
RemoteResponse response = new RemoteResponse();
|
||
|
response.mPendingIntent = pendingIntent;
|
||
|
return response;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is
|
||
|
* very costly to set PendingIntents on the individual items, and is hence not recommended.
|
||
|
* Instead a single PendingIntent template can be set on the collection, see {@link
|
||
|
* RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click
|
||
|
* action of a given item can be distinguished by setting a fillInIntent on that item. The
|
||
|
* fillInIntent is then combined with the PendingIntent template in order to determine the
|
||
|
* final intent which will be executed when the item is clicked. This works as follows: any
|
||
|
* fields which are left blank in the PendingIntent template, but are provided by the
|
||
|
* fillInIntent will be overwritten, and the resulting PendingIntent will be used. The rest
|
||
|
* of the PendingIntent template will then be filled in with the associated fields that are
|
||
|
* set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details.
|
||
|
* Creates a response which sends a pending intent as part of the response. The source
|
||
|
* bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the
|
||
|
* target view in screen space.
|
||
|
* Note that any activity options associated with the mPendingIntent may get overridden
|
||
|
* before starting the intent.
|
||
|
*
|
||
|
* @param fillIntent The intent which will be combined with the parent's PendingIntent in
|
||
|
* order to determine the behavior of the response
|
||
|
* @see RemoteViews#setPendingIntentTemplate(int, PendingIntent)
|
||
|
* @see RemoteViews#setOnClickFillInIntent(int, Intent)
|
||
|
*/
|
||
|
@NonNull
|
||
|
public static RemoteResponse fromFillInIntent(@NonNull Intent fillIntent) {
|
||
|
RemoteResponse response = new RemoteResponse();
|
||
|
response.mFillIntent = fillIntent;
|
||
|
return response;
|
||
|
}
|
||
|
|
||
|
private static RemoteResponse fromPendingIntentTemplateAndFillInIntent(
|
||
|
@NonNull final PendingIntent pendingIntent, @NonNull final Intent intent) {
|
||
|
RemoteResponse response = new RemoteResponse();
|
||
|
response.mPendingIntent = pendingIntent;
|
||
|
response.mFillIntent = intent;
|
||
|
return response;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a shared element to be transferred as part of the transition between Activities
|
||
|
* using cross-Activity scene animations. The position of the first element will be used as
|
||
|
* the epicenter for the exit Transition. The position of the associated shared element in
|
||
|
* the launched Activity will be the epicenter of its entering Transition.
|
||
|
*
|
||
|
* @param viewId The id of the view to be shared as part of the transition
|
||
|
* @param sharedElementName The shared element name for this view
|
||
|
* @see ActivityOptions#makeSceneTransitionAnimation(Activity, Pair[])
|
||
|
*/
|
||
|
@NonNull
|
||
|
public RemoteResponse addSharedElement(@IdRes int viewId,
|
||
|
@NonNull String sharedElementName) {
|
||
|
if (mViewIds == null) {
|
||
|
mViewIds = new IntArray();
|
||
|
mElementNames = new ArrayList<>();
|
||
|
}
|
||
|
mViewIds.add(viewId);
|
||
|
mElementNames.add(sharedElementName);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the interaction type for which this RemoteResponse responds.
|
||
|
*
|
||
|
* @param type the type of interaction for which this is a response, such as clicking or
|
||
|
* checked state changing
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@NonNull
|
||
|
public RemoteResponse setInteractionType(@InteractionType int type) {
|
||
|
mInteractionType = type;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
private void writeToParcel(Parcel dest, int flags) {
|
||
|
PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
|
||
|
dest.writeBoolean((mFillIntent != null));
|
||
|
if (mFillIntent != null) {
|
||
|
dest.writeTypedObject(mFillIntent, flags);
|
||
|
}
|
||
|
dest.writeInt(mInteractionType);
|
||
|
dest.writeIntArray(mViewIds == null ? null : mViewIds.toArray());
|
||
|
dest.writeStringList(mElementNames);
|
||
|
}
|
||
|
|
||
|
private void readFromParcel(Parcel parcel) {
|
||
|
mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
|
||
|
mFillIntent = parcel.readBoolean() ? parcel.readTypedObject(Intent.CREATOR) : null;
|
||
|
mInteractionType = parcel.readInt();
|
||
|
int[] viewIds = parcel.createIntArray();
|
||
|
mViewIds = viewIds == null ? null : IntArray.wrap(viewIds);
|
||
|
mElementNames = parcel.createStringArrayList();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* See {@link RemoteViews#visitUris(Consumer)}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
if (mPendingIntent != null) {
|
||
|
mPendingIntent.visitUris(visitor);
|
||
|
}
|
||
|
if (mFillIntent != null) {
|
||
|
mFillIntent.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void handleViewInteraction(
|
||
|
View v,
|
||
|
InteractionHandler handler) {
|
||
|
final PendingIntent pi;
|
||
|
if (mPendingIntent != null) {
|
||
|
pi = mPendingIntent;
|
||
|
} else if (mFillIntent != null) {
|
||
|
AdapterView<?> ancestor = getAdapterViewAncestor(v);
|
||
|
if (ancestor == null) {
|
||
|
Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Ensure that a template pending intent has been set on the ancestor
|
||
|
if (!(ancestor.getTag() instanceof PendingIntent)) {
|
||
|
Log.e(LOG_TAG, "Attempting setOnClickFillInIntent or "
|
||
|
+ "setOnCheckedChangeFillInIntent without calling "
|
||
|
+ "setPendingIntentTemplate on parent.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
pi = (PendingIntent) ancestor.getTag();
|
||
|
} else {
|
||
|
Log.e(LOG_TAG, "Response has neither pendingIntent nor fillInIntent");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
handler.onInteraction(v, pi, this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the closest ancestor of the view that is an AdapterView or null if none could be
|
||
|
* found.
|
||
|
*/
|
||
|
@Nullable
|
||
|
private static AdapterView<?> getAdapterViewAncestor(@Nullable View view) {
|
||
|
if (view == null) return null;
|
||
|
|
||
|
View parent = (View) view.getParent();
|
||
|
// Break the for loop on the first encounter of:
|
||
|
// 1) an AdapterView,
|
||
|
// 2) an AppWidgetHostView that is not a child of an adapter view, or
|
||
|
// 3) a null parent.
|
||
|
// 2) and 3) are unexpected and catch the case where a child is not
|
||
|
// correctly parented in an AdapterView.
|
||
|
while (parent != null && !(parent instanceof AdapterView<?>)
|
||
|
&& !((parent instanceof AppWidgetHostView)
|
||
|
&& !(parent instanceof AppWidgetHostView.AdapterChildHostView))) {
|
||
|
parent = (View) parent.getParent();
|
||
|
}
|
||
|
|
||
|
return parent instanceof AdapterView<?> ? (AdapterView<?>) parent : null;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public Pair<Intent, ActivityOptions> getLaunchOptions(View view) {
|
||
|
Intent intent = mFillIntent == null ? new Intent() : new Intent(mFillIntent);
|
||
|
intent.setSourceBounds(getSourceBounds(view));
|
||
|
|
||
|
if (view instanceof CompoundButton
|
||
|
&& mInteractionType == INTERACTION_TYPE_CHECKED_CHANGE) {
|
||
|
intent.putExtra(EXTRA_CHECKED, ((CompoundButton) view).isChecked());
|
||
|
}
|
||
|
|
||
|
ActivityOptions opts = null;
|
||
|
|
||
|
Context context = view.getContext();
|
||
|
if (context.getResources().getBoolean(
|
||
|
com.android.internal.R.bool.config_overrideRemoteViewsActivityTransition)) {
|
||
|
TypedArray windowStyle = context.getTheme().obtainStyledAttributes(
|
||
|
com.android.internal.R.styleable.Window);
|
||
|
int windowAnimations = windowStyle.getResourceId(
|
||
|
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
|
||
|
TypedArray windowAnimationStyle = context.obtainStyledAttributes(
|
||
|
windowAnimations, com.android.internal.R.styleable.WindowAnimation);
|
||
|
int enterAnimationId = windowAnimationStyle.getResourceId(com.android.internal.R
|
||
|
.styleable.WindowAnimation_activityOpenRemoteViewsEnterAnimation, 0);
|
||
|
windowStyle.recycle();
|
||
|
windowAnimationStyle.recycle();
|
||
|
|
||
|
if (enterAnimationId != 0) {
|
||
|
opts = ActivityOptions.makeCustomAnimation(context,
|
||
|
enterAnimationId, 0);
|
||
|
opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (opts == null && mViewIds != null && mElementNames != null) {
|
||
|
View parent = (View) view.getParent();
|
||
|
while (parent != null && !(parent instanceof AppWidgetHostView)) {
|
||
|
parent = (View) parent.getParent();
|
||
|
}
|
||
|
if (parent instanceof AppWidgetHostView) {
|
||
|
opts = ((AppWidgetHostView) parent).createSharedElementActivityOptions(
|
||
|
mViewIds.toArray(),
|
||
|
mElementNames.toArray(new String[mElementNames.size()]), intent);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (opts == null) {
|
||
|
opts = ActivityOptions.makeBasic();
|
||
|
opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||
|
}
|
||
|
if (view.getDisplay() != null) {
|
||
|
opts.setLaunchDisplayId(view.getDisplay().getDisplayId());
|
||
|
} else {
|
||
|
// TODO(b/218409359): Remove once bug is fixed.
|
||
|
Log.w(LOG_TAG, "getLaunchOptions: view.getDisplay() is null!",
|
||
|
new Exception());
|
||
|
}
|
||
|
// If the user interacts with a visible element it is safe to assume they consent that
|
||
|
// something is going to start.
|
||
|
opts.setPendingIntentBackgroundActivityStartMode(
|
||
|
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
|
||
|
opts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
|
||
|
return Pair.create(intent, opts);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public static boolean startPendingIntent(View view, PendingIntent pendingIntent,
|
||
|
Pair<Intent, ActivityOptions> options) {
|
||
|
try {
|
||
|
// TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
|
||
|
Context context = view.getContext();
|
||
|
// The NEW_TASK flags are applied through the activity options and not as a part of
|
||
|
// the call to startIntentSender() to ensure that they are consistently applied to
|
||
|
// both mutable and immutable PendingIntents.
|
||
|
context.startIntentSender(
|
||
|
pendingIntent.getIntentSender(), options.first,
|
||
|
0, 0, 0, options.second.toBundle());
|
||
|
} catch (IntentSender.SendIntentException e) {
|
||
|
Log.e(LOG_TAG, "Cannot send pending intent: ", e);
|
||
|
return false;
|
||
|
} catch (Exception e) {
|
||
|
Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** Representation of a fixed list of items to be displayed in a RemoteViews collection. */
|
||
|
public static final class RemoteCollectionItems implements Parcelable {
|
||
|
private final long[] mIds;
|
||
|
private final RemoteViews[] mViews;
|
||
|
private final boolean mHasStableIds;
|
||
|
private final int mViewTypeCount;
|
||
|
|
||
|
private HierarchyRootData mHierarchyRootData;
|
||
|
|
||
|
RemoteCollectionItems(
|
||
|
long[] ids, RemoteViews[] views, boolean hasStableIds, int viewTypeCount) {
|
||
|
mIds = ids;
|
||
|
mViews = views;
|
||
|
mHasStableIds = hasStableIds;
|
||
|
mViewTypeCount = viewTypeCount;
|
||
|
if (ids.length != views.length) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"RemoteCollectionItems has different number of ids and views");
|
||
|
}
|
||
|
if (viewTypeCount < 1) {
|
||
|
throw new IllegalArgumentException("View type count must be >= 1");
|
||
|
}
|
||
|
int layoutIdCount = (int) Arrays.stream(views)
|
||
|
.mapToInt(RemoteViews::getLayoutId)
|
||
|
.distinct()
|
||
|
.count();
|
||
|
if (layoutIdCount > viewTypeCount) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"View type count is set to " + viewTypeCount + ", but the collection "
|
||
|
+ "contains " + layoutIdCount + " different layout ids");
|
||
|
}
|
||
|
|
||
|
// Until the collection items are attached to a parent, we configure the first item
|
||
|
// to be the root of the others to share caches and save space during serialization.
|
||
|
if (views.length > 0) {
|
||
|
setHierarchyRootData(views[0].getHierarchyRootData());
|
||
|
views[0].mIsRoot = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
RemoteCollectionItems(@NonNull Parcel in, @Nullable HierarchyRootData hierarchyRootData) {
|
||
|
mHasStableIds = in.readBoolean();
|
||
|
mViewTypeCount = in.readInt();
|
||
|
int length = in.readInt();
|
||
|
mIds = new long[length];
|
||
|
in.readLongArray(mIds);
|
||
|
|
||
|
boolean attached = in.readBoolean();
|
||
|
mViews = new RemoteViews[length];
|
||
|
int firstChildIndex;
|
||
|
if (attached) {
|
||
|
if (hierarchyRootData == null) {
|
||
|
throw new IllegalStateException("Cannot unparcel a RemoteCollectionItems that "
|
||
|
+ "was parceled as attached without providing data for a root "
|
||
|
+ "RemoteViews");
|
||
|
}
|
||
|
mHierarchyRootData = hierarchyRootData;
|
||
|
firstChildIndex = 0;
|
||
|
} else {
|
||
|
mViews[0] = new RemoteViews(in);
|
||
|
mHierarchyRootData = mViews[0].getHierarchyRootData();
|
||
|
firstChildIndex = 1;
|
||
|
}
|
||
|
|
||
|
for (int i = firstChildIndex; i < length; i++) {
|
||
|
mViews[i] = new RemoteViews(
|
||
|
in,
|
||
|
mHierarchyRootData,
|
||
|
/* info= */ null,
|
||
|
/* depth= */ 0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void setHierarchyRootData(@NonNull HierarchyRootData rootData) {
|
||
|
mHierarchyRootData = rootData;
|
||
|
for (RemoteViews view : mViews) {
|
||
|
view.configureAsChild(rootData);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||
|
writeToParcel(dest, flags, /* attached= */ false);
|
||
|
}
|
||
|
|
||
|
private void writeToParcel(@NonNull Parcel dest, int flags, boolean attached) {
|
||
|
boolean prevAllowSquashing = dest.allowSquashing();
|
||
|
|
||
|
dest.writeBoolean(mHasStableIds);
|
||
|
dest.writeInt(mViewTypeCount);
|
||
|
dest.writeInt(mIds.length);
|
||
|
dest.writeLongArray(mIds);
|
||
|
|
||
|
if (attached && mHierarchyRootData == null) {
|
||
|
throw new IllegalStateException("Cannot call writeToParcelAttached for a "
|
||
|
+ "RemoteCollectionItems without first calling setHierarchyRootData()");
|
||
|
}
|
||
|
|
||
|
// Write whether we parceled as attached or not. This allows cleaner validation and
|
||
|
// proper error messaging when unparceling later.
|
||
|
dest.writeBoolean(attached);
|
||
|
boolean restoreRoot = false;
|
||
|
if (!attached && mViews.length > 0 && !mViews[0].mIsRoot) {
|
||
|
// If we're writing unattached, temporarily set the first item as the root so that
|
||
|
// the bitmap cache is written to the parcel.
|
||
|
restoreRoot = true;
|
||
|
mViews[0].mIsRoot = true;
|
||
|
}
|
||
|
|
||
|
for (RemoteViews view : mViews) {
|
||
|
view.writeToParcel(dest, flags);
|
||
|
}
|
||
|
|
||
|
if (restoreRoot) mViews[0].mIsRoot = false;
|
||
|
dest.restoreAllowSquashing(prevAllowSquashing);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the id for {@code position}. See {@link #hasStableIds()} for whether this id
|
||
|
* should be considered meaningful across collection updates.
|
||
|
*
|
||
|
* @return Id for the position.
|
||
|
*/
|
||
|
public long getItemId(int position) {
|
||
|
return mIds[position];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the {@link RemoteViews} to display at {@code position}.
|
||
|
*
|
||
|
* @return RemoteViews for the position.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public RemoteViews getItemView(int position) {
|
||
|
return mViews[position];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the number of elements in the collection.
|
||
|
*
|
||
|
* @return Count of items.
|
||
|
*/
|
||
|
public int getItemCount() {
|
||
|
return mIds.length;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the view type count for the collection when used in an adapter
|
||
|
*
|
||
|
* @return Count of view types for the collection when used in an adapter.
|
||
|
* @see android.widget.Adapter#getViewTypeCount()
|
||
|
*/
|
||
|
public int getViewTypeCount() {
|
||
|
return mViewTypeCount;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Indicates whether the item ids are stable across changes to the underlying data.
|
||
|
*
|
||
|
* @return True if the same id always refers to the same object.
|
||
|
* @see android.widget.Adapter#hasStableIds()
|
||
|
*/
|
||
|
public boolean hasStableIds() {
|
||
|
return mHasStableIds;
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
public static final Creator<RemoteCollectionItems> CREATOR =
|
||
|
new Creator<RemoteCollectionItems>() {
|
||
|
@NonNull
|
||
|
@Override
|
||
|
public RemoteCollectionItems createFromParcel(@NonNull Parcel source) {
|
||
|
return new RemoteCollectionItems(source, /* hierarchyRoot= */ null);
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
@Override
|
||
|
public RemoteCollectionItems[] newArray(int size) {
|
||
|
return new RemoteCollectionItems[size];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/** Builder class for {@link RemoteCollectionItems} objects.*/
|
||
|
public static final class Builder {
|
||
|
private final LongArray mIds = new LongArray();
|
||
|
private final List<RemoteViews> mViews = new ArrayList<>();
|
||
|
private boolean mHasStableIds;
|
||
|
private int mViewTypeCount;
|
||
|
|
||
|
/**
|
||
|
* Adds a {@link RemoteViews} to the collection.
|
||
|
*
|
||
|
* @param id Id to associate with the row. Use {@link #setHasStableIds(boolean)} to
|
||
|
* indicate that ids are stable across changes to the collection.
|
||
|
* @param view RemoteViews to display for the row.
|
||
|
*/
|
||
|
@NonNull
|
||
|
// Covered by getItemId, getItemView, getItemCount.
|
||
|
@SuppressLint("MissingGetterMatchingBuilder")
|
||
|
public Builder addItem(long id, @NonNull RemoteViews view) {
|
||
|
if (view == null) throw new NullPointerException();
|
||
|
if (view.hasMultipleLayouts()) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"RemoteViews used in a RemoteCollectionItems cannot specify separate "
|
||
|
+ "layouts for orientations or sizes.");
|
||
|
}
|
||
|
mIds.add(id);
|
||
|
mViews.add(view);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets whether the item ids are stable across changes to the underlying data.
|
||
|
*
|
||
|
* @see android.widget.Adapter#hasStableIds()
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Builder setHasStableIds(boolean hasStableIds) {
|
||
|
mHasStableIds = hasStableIds;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the view type count for the collection when used in an adapter. This can be set
|
||
|
* to the maximum number of different layout ids that will be used by RemoteViews in
|
||
|
* this collection.
|
||
|
*
|
||
|
* If this value is not set, then a value will be inferred from the provided items. As
|
||
|
* a result, the adapter may need to be recreated when the list is updated with
|
||
|
* previously unseen RemoteViews layouts for new items.
|
||
|
*
|
||
|
* @see android.widget.Adapter#getViewTypeCount()
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Builder setViewTypeCount(int viewTypeCount) {
|
||
|
mViewTypeCount = viewTypeCount;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/** Creates the {@link RemoteCollectionItems} defined by this builder. */
|
||
|
@NonNull
|
||
|
public RemoteCollectionItems build() {
|
||
|
if (mViewTypeCount < 1) {
|
||
|
// If a view type count wasn't specified, set it to be the number of distinct
|
||
|
// layout ids used in the items.
|
||
|
mViewTypeCount = (int) mViews.stream()
|
||
|
.mapToInt(RemoteViews::getLayoutId)
|
||
|
.distinct()
|
||
|
.count();
|
||
|
}
|
||
|
return new RemoteCollectionItems(
|
||
|
mIds.toArray(),
|
||
|
mViews.toArray(new RemoteViews[0]),
|
||
|
mHasStableIds,
|
||
|
Math.max(mViewTypeCount, 1));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* See {@link RemoteViews#visitUris(Consumer)}.
|
||
|
*/
|
||
|
private void visitUris(@NonNull Consumer<Uri> visitor) {
|
||
|
for (RemoteViews view : mViews) {
|
||
|
view.visitUris(visitor);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A data parcel that carries the instructions to draw the RemoteViews, as an alternative to
|
||
|
* XML layout.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public static final class DrawInstructions {
|
||
|
|
||
|
private static final long VERSION = 1L;
|
||
|
|
||
|
@NonNull
|
||
|
final List<byte[]> mInstructions;
|
||
|
|
||
|
private DrawInstructions() {
|
||
|
throw new UnsupportedOperationException(
|
||
|
"DrawInstructions cannot be instantiate without instructions");
|
||
|
}
|
||
|
|
||
|
private DrawInstructions(@NonNull List<byte[]> instructions) {
|
||
|
// Create and retain an immutable copy of given instructions.
|
||
|
mInstructions = new ArrayList<>(instructions.size());
|
||
|
for (byte[] instruction : instructions) {
|
||
|
final int len = instruction.length;
|
||
|
final byte[] target = new byte[len];
|
||
|
System.arraycopy(instruction, 0, target, 0, len);
|
||
|
mInstructions.add(target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static DrawInstructions readFromParcel(@NonNull final Parcel in) {
|
||
|
int size = in.readInt();
|
||
|
if (size == -1) {
|
||
|
return null;
|
||
|
}
|
||
|
byte[] instruction;
|
||
|
final List<byte[]> instructions = new ArrayList<>(size);
|
||
|
for (int i = 0; i < size; i++) {
|
||
|
instruction = in.readBlob();
|
||
|
instructions.add(instruction);
|
||
|
}
|
||
|
return new DrawInstructions(instructions);
|
||
|
}
|
||
|
|
||
|
private static void writeToParcel(@Nullable final DrawInstructions drawInstructions,
|
||
|
@NonNull final Parcel dest, final int flags) {
|
||
|
if (drawInstructions == null) {
|
||
|
dest.writeInt(-1);
|
||
|
return;
|
||
|
}
|
||
|
final List<byte[]> instructions = drawInstructions.mInstructions;
|
||
|
dest.writeInt(instructions.size());
|
||
|
for (byte[] instruction : instructions) {
|
||
|
dest.writeBlob(instruction);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Version number of {@link DrawInstructions} currently supported.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public static long getSupportedVersion() {
|
||
|
return VERSION;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Builder class for {@link DrawInstructions} objects.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public static final class Builder {
|
||
|
|
||
|
private final List<byte[]> mInstructions;
|
||
|
|
||
|
/**
|
||
|
* Constructor.
|
||
|
*
|
||
|
* @param instructions Information to draw the RemoteViews.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public Builder(@NonNull final List<byte[]> instructions) {
|
||
|
mInstructions = new ArrayList<>(instructions);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a {@link DrawInstructions} instance.
|
||
|
*/
|
||
|
@NonNull
|
||
|
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
|
||
|
public DrawInstructions build() {
|
||
|
return new DrawInstructions(mInstructions);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the ID of the top-level view of the XML layout, if set using
|
||
|
* {@link RemoteViews#RemoteViews(String, int, int)}.
|
||
|
*/
|
||
|
@IdRes
|
||
|
public int getViewId() {
|
||
|
return mViewId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the provider instance ID.
|
||
|
*
|
||
|
* This should only be used by {@link com.android.server.appwidget.AppWidgetService}.
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setProviderInstanceId(long id) {
|
||
|
mProviderInstanceId = id;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the provider instance id.
|
||
|
*
|
||
|
* This should uniquely identifies {@link RemoteViews} coming from a given App Widget
|
||
|
* Provider. This changes each time the App Widget provider update the {@link RemoteViews} of
|
||
|
* its widget. Returns -1 if the {@link RemoteViews} doesn't come from an App Widget provider.
|
||
|
* @hide
|
||
|
*/
|
||
|
public long getProviderInstanceId() {
|
||
|
return mProviderInstanceId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Identify the child of this {@link RemoteViews}, or 0 if this is not a child.
|
||
|
*
|
||
|
* The returned value is always a small integer, currently between 0 and 17.
|
||
|
*/
|
||
|
private int getChildId(@NonNull RemoteViews child) {
|
||
|
if (child == this) {
|
||
|
return 0;
|
||
|
}
|
||
|
if (hasSizedRemoteViews()) {
|
||
|
for (int i = 0; i < mSizedRemoteViews.size(); i++) {
|
||
|
if (mSizedRemoteViews.get(i) == child) {
|
||
|
return i + 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (hasLandscapeAndPortraitLayouts()) {
|
||
|
if (mLandscape == child) {
|
||
|
return 1;
|
||
|
} else if (mPortrait == child) {
|
||
|
return 2;
|
||
|
}
|
||
|
}
|
||
|
// This is not a child of this RemoteViews.
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Identify uniquely this RemoteViews, or returns -1 if not possible.
|
||
|
*
|
||
|
* @param parent If the {@link RemoteViews} is not a root {@link RemoteViews}, this should be
|
||
|
* the parent that contains it.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public long computeUniqueId(@Nullable RemoteViews parent) {
|
||
|
if (mIsRoot) {
|
||
|
long viewId = getProviderInstanceId();
|
||
|
if (viewId != -1) {
|
||
|
viewId <<= 8;
|
||
|
}
|
||
|
return viewId;
|
||
|
}
|
||
|
if (parent == null) {
|
||
|
return -1;
|
||
|
}
|
||
|
long viewId = parent.getProviderInstanceId();
|
||
|
if (viewId == -1) {
|
||
|
return -1;
|
||
|
}
|
||
|
int childId = parent.getChildId(this);
|
||
|
if (childId == -1) {
|
||
|
return -1;
|
||
|
}
|
||
|
viewId <<= 8;
|
||
|
viewId |= childId;
|
||
|
return viewId;
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private static Pair<String, Integer> getPackageUserKey(@Nullable ApplicationInfo info) {
|
||
|
if (info == null || info.packageName == null) return null;
|
||
|
return Pair.create(info.packageName, info.uid);
|
||
|
}
|
||
|
|
||
|
private HierarchyRootData getHierarchyRootData() {
|
||
|
return new HierarchyRootData(mBitmapCache, mCollectionCache,
|
||
|
mApplicationInfoCache, mClassCookies);
|
||
|
}
|
||
|
|
||
|
private static final class HierarchyRootData {
|
||
|
final BitmapCache mBitmapCache;
|
||
|
final RemoteCollectionCache mRemoteCollectionCache;
|
||
|
final ApplicationInfoCache mApplicationInfoCache;
|
||
|
final Map<Class, Object> mClassCookies;
|
||
|
|
||
|
HierarchyRootData(
|
||
|
BitmapCache bitmapCache,
|
||
|
RemoteCollectionCache remoteCollectionCache,
|
||
|
ApplicationInfoCache applicationInfoCache,
|
||
|
Map<Class, Object> classCookies) {
|
||
|
mBitmapCache = bitmapCache;
|
||
|
mRemoteCollectionCache = remoteCollectionCache;
|
||
|
mApplicationInfoCache = applicationInfoCache;
|
||
|
mClassCookies = classCookies;
|
||
|
}
|
||
|
}
|
||
|
}
|