diff options
10 files changed, 429 insertions, 258 deletions
diff --git a/api/current.txt b/api/current.txt index d137616d9690..2f69e88ca077 100755 --- a/api/current.txt +++ b/api/current.txt @@ -54397,6 +54397,7 @@ package android.widget { method public void setLong(int, java.lang.String, long); method public void setOnClickFillInIntent(int, android.content.Intent); method public void setOnClickPendingIntent(int, android.app.PendingIntent); + method public void setOnClickResponse(int, android.widget.RemoteViews.RemoteResponse); method public void setPendingIntentTemplate(int, android.app.PendingIntent); method public void setProgressBar(int, int, int, boolean); method public void setRelativeScrollPosition(int, int); @@ -54417,6 +54418,7 @@ package android.widget { method public void showPrevious(int); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.widget.RemoteViews> CREATOR; + field public static final java.lang.String EXTRA_SHARED_ELEMENT_BOUNDS = "android.widget.extra.SHARED_ELEMENT_BOUNDS"; } public static class RemoteViews.ActionException extends java.lang.RuntimeException { @@ -54424,6 +54426,13 @@ package android.widget { ctor public RemoteViews.ActionException(java.lang.String); } + public static class RemoteViews.RemoteResponse { + ctor public RemoteViews.RemoteResponse(); + method public android.widget.RemoteViews.RemoteResponse addSharedElement(int, java.lang.String); + method public static android.widget.RemoteViews.RemoteResponse fromFillInIntent(android.content.Intent); + method public static android.widget.RemoteViews.RemoteResponse fromPendingIntent(android.app.PendingIntent); + } + public static abstract class RemoteViews.RemoteView implements java.lang.annotation.Annotation { } diff --git a/config/hiddenapi-light-greylist.txt b/config/hiddenapi-light-greylist.txt index d4465ebb4a90..22c11e4625f7 100644 --- a/config/hiddenapi-light-greylist.txt +++ b/config/hiddenapi-light-greylist.txt @@ -1455,7 +1455,6 @@ Landroid/webkit/IWebViewUpdateService;->getCurrentWebViewPackageName()Ljava/lang Landroid/webkit/IWebViewUpdateService;->getValidWebViewPackages()[Landroid/webkit/WebViewProviderInfo; Landroid/webkit/IWebViewUpdateService;->isFallbackPackage(Ljava/lang/String;)Z Landroid/widget/RelativeLayout$DependencyGraph$Node;-><init>()V -Landroid/widget/RemoteViews$OnClickHandler;-><init>()V Landroid/widget/ScrollBarDrawable;-><init>()V Lcom/android/ims/ImsConfigListener$Stub;-><init>()V Lcom/android/ims/internal/IImsCallSession$Stub;-><init>()V diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index a9187b65a359..c6e94c722531 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -17,8 +17,12 @@ package android.appwidget; import android.annotation.UnsupportedAppUsage; +import android.app.Activity; +import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; @@ -30,6 +34,7 @@ import android.os.CancellationSignal; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; @@ -44,6 +49,8 @@ import android.widget.RemoteViews.OnClickHandler; import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executor; /** @@ -651,4 +658,39 @@ public class AppWidgetHostView extends FrameLayout { super.onInitializeAccessibilityNodeInfoInternal(info); info.setClassName(AppWidgetHostView.class.getName()); } + + /** @hide */ + public ActivityOptions createSharedElementActivityOptions( + int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) { + Context parentContext = getContext(); + while ((parentContext instanceof ContextWrapper) + && !(parentContext instanceof Activity)) { + parentContext = ((ContextWrapper) parentContext).getBaseContext(); + } + if (!(parentContext instanceof Activity)) { + return null; + } + + List<Pair<View, String>> sharedElements = new ArrayList<>(); + Bundle extras = new Bundle(); + + for (int i = 0; i < sharedViewIds.length; i++) { + View view = findViewById(sharedViewIds[i]); + if (view != null) { + sharedElements.add(Pair.create(view, sharedViewNames[i])); + + extras.putParcelable(sharedViewNames[i], RemoteViews.getSourceBounds(view)); + } + } + + if (!sharedElements.isEmpty()) { + fillInIntent.putExtra(RemoteViews.EXTRA_SHARED_ELEMENT_BOUNDS, extras); + final ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation( + (Activity) parentContext, + sharedElements.toArray(new Pair[sharedElements.size()])); + opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return opts; + } + return null; + } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 4d031232152a..a93604f95c79 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -21,6 +21,7 @@ import android.annotation.DimenRes; import android.annotation.NonNull; import android.annotation.StyleRes; import android.annotation.UnsupportedAppUsage; +import android.app.Activity; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; @@ -56,13 +57,14 @@ import android.os.StrictMode; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.IntArray; import android.util.Log; +import android.util.Pair; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; import android.view.RemotableViewMethod; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.AdapterView.OnItemClickListener; @@ -129,13 +131,19 @@ public class RemoteViews implements Parcelable, Filter { static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId"; /** + * The intent extra that contains the bounds for all shared elements. + */ + public static final String EXTRA_SHARED_ELEMENT_BOUNDS = + "android.widget.extra.SHARED_ELEMENT_BOUNDS"; + + /** * Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and * {@link #RemoteViews(RemoteViews, RemoteViews)}. */ private static final int MAX_NESTED_VIEWS = 10; // The unique identifiers for each custom {@link Action}. - private static final int SET_ON_CLICK_PENDING_INTENT_TAG = 1; + 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; @@ -143,7 +151,6 @@ public class RemoteViews implements Parcelable, Filter { 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_ON_CLICK_FILL_IN_INTENT_TAG = 9; 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; @@ -228,7 +235,8 @@ public class RemoteViews implements Parcelable, Filter { /** Class cookies of the Parcel this instance was read from. */ private final Map<Class, Object> mClassCookies; - private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = new OnClickHandler(); + private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = (view, pendingIntent, response) + -> startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)); private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>(); @@ -362,57 +370,10 @@ public class RemoteViews implements Parcelable, Filter { } /** @hide */ - public static class OnClickHandler { - - @UnsupportedAppUsage - public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { - try { - // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? - Context context = view.getContext(); - ActivityOptions opts = getActivityOptions(context); - // 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(), fillInIntent, - 0, 0, 0, opts.toBundle()); - } catch (IntentSender.SendIntentException e) { - android.util.Log.e(LOG_TAG, "Cannot send pending intent: ", e); - return false; - } catch (Exception e) { - android.util.Log.e(LOG_TAG, "Cannot send pending intent due to " + - "unknown exception: ", e); - return false; - } - return true; - } + public interface OnClickHandler { /** @hide */ - protected ActivityOptions getActivityOptions(Context context) { - 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) { - final ActivityOptions opts = ActivityOptions.makeCustomAnimation(context, - enterAnimationId, 0); - opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return opts; - } - } - final ActivityOptions opts = ActivityOptions.makeBasic(); - opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return opts; - } + boolean onClickHandler(View view, PendingIntent pendingIntent, RemoteResponse response); } /** @@ -630,86 +591,6 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetOnClickFillInIntent extends Action { - public SetOnClickFillInIntent(int id, Intent fillInIntent) { - this.viewId = id; - this.fillInIntent = fillInIntent; - } - - public SetOnClickFillInIntent(Parcel parcel) { - viewId = parcel.readInt(); - fillInIntent = parcel.readTypedObject(Intent.CREATOR); - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(viewId); - dest.writeTypedObject(fillInIntent, 0 /* no flags */); - } - - @Override - public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) { - final View target = root.findViewById(viewId); - if (target == null) return; - - if (!mIsWidgetCollectionChild) { - Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " + - "only from RemoteViewsFactory (ie. on collection items)."); - return; - } - if (target == root) { - target.setTagInternal(com.android.internal.R.id.fillInIntent, fillInIntent); - } else if (fillInIntent != null) { - OnClickListener listener = new OnClickListener() { - public void onClick(View v) { - // Insure that this view is a child of an AdapterView - View parent = (View) v.getParent(); - // Break the for loop on the first encounter of: - // 1) an AdapterView, - // 2) an AppWidgetHostView that is not a RemoteViewsFrameLayout, or - // 3) a null parent. - // 2) and 3) are unexpected and catch the case where a child is not - // correctly parented in an AdapterView. - while (parent != null && !(parent instanceof AdapterView<?>) - && !((parent instanceof AppWidgetHostView) && - !(parent instanceof RemoteViewsAdapter.RemoteViewsFrameLayout))) { - parent = (View) parent.getParent(); - } - - if (!(parent instanceof AdapterView<?>)) { - // Somehow they've managed to get this far without having - // and AdapterView as a parent. - Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent"); - return; - } - - // Insure that a template pending intent has been set on an ancestor - if (!(parent.getTag() instanceof PendingIntent)) { - Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without" + - " calling setPendingIntentTemplate on parent."); - return; - } - - PendingIntent pendingIntent = (PendingIntent) parent.getTag(); - - final Rect rect = getSourceBounds(v); - - fillInIntent.setSourceBounds(rect); - handler.onClickHandler(v, pendingIntent, fillInIntent); - } - - }; - target.setOnClickListener(listener); - } - } - - @Override - public int getActionTag() { - return SET_ON_CLICK_FILL_IN_INTENT_TAG; - } - - Intent fillInIntent; - } - private class SetPendingIntentTemplate extends Action { public SetPendingIntentTemplate(int id, PendingIntent pendingIntentTemplate) { this.viewId = id; @@ -749,22 +630,17 @@ public class RemoteViews implements Parcelable, Filter { } if (vg == null) return; - Intent fillInIntent = null; + RemoteResponse response = null; int childCount = vg.getChildCount(); for (int i = 0; i < childCount; i++) { Object tag = vg.getChildAt(i).getTag(com.android.internal.R.id.fillInIntent); - if (tag instanceof Intent) { - fillInIntent = (Intent) tag; + if (tag instanceof RemoteResponse) { + response = (RemoteResponse) tag; break; } } - if (fillInIntent == null) return; - - final Rect rect = getSourceBounds(view); - - final Intent intent = new Intent(); - intent.setSourceBounds(rect); - handler.onClickHandler(view, pendingIntentTemplate, fillInIntent); + if (response == null) return; + response.handleViewClick(view, handler); } } }; @@ -922,20 +798,22 @@ public class RemoteViews implements Parcelable, Filter { * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} * to launch the provided {@link PendingIntent}. */ - private class SetOnClickPendingIntent extends Action { - public SetOnClickPendingIntent(int id, PendingIntent pendingIntent) { + private class SetOnClickResponse extends Action { + + SetOnClickResponse(int id, RemoteResponse response) { this.viewId = id; - this.pendingIntent = pendingIntent; + this.mResponse = response; } - public SetOnClickPendingIntent(Parcel parcel) { + SetOnClickResponse(Parcel parcel) { viewId = parcel.readInt(); - pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); + mResponse = new RemoteResponse(); + mResponse.readFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(viewId); - PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, dest); + mResponse.writeToParcel(dest, flags); } @Override @@ -943,50 +821,54 @@ public class RemoteViews implements Parcelable, Filter { final View target = root.findViewById(viewId); if (target == null) return; - // 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 (mIsWidgetCollectionChild) { - Log.w(LOG_TAG, "Cannot setOnClickPendingIntent for collection item " + - "(id: " + viewId + ")"); - ApplicationInfo appInfo = root.getContext().getApplicationInfo(); - - // We let this slide for HC and ICS so as to not break compatibility. It should have - // been disabled from the outset, but was left open by accident. - if (appInfo != null && - appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) { + 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 (mIsWidgetCollectionChild) { + Log.w(LOG_TAG, "Cannot SetOnClickResponse for collection item " + + "(id: " + viewId + ")"); + ApplicationInfo appInfo = root.getContext().getApplicationInfo(); + + // We let this slide for HC and ICS so as to not break compatibility. It should + // have been disabled from the outset, but was left open by accident. + if (appInfo != null + && appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) { + return; + } + } + target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent); + } else if (mResponse.mFillIntent != null) { + if (!mIsWidgetCollectionChild) { + Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " + + "only from RemoteViewsFactory (ie. on collection items)."); return; } + if (target == root) { + // Target is a root node of an AdapterView child. Set the response in the tag. + // Actual click handling is done by OnItemClickListener in + // SetPendingIntentTemplate, which uses this tag information. + target.setTagInternal(com.android.internal.R.id.fillInIntent, mResponse); + return; + } + } else { + // No intent to apply + target.setOnClickListener(null); + return; } - - // If the pendingIntent is null, we clear the onClickListener - OnClickListener listener = null; - if (pendingIntent != null) { - listener = new OnClickListener() { - public void onClick(View v) { - // Find target view location in screen coordinates and - // fill into PendingIntent before sending. - final Rect rect = getSourceBounds(v); - - final Intent intent = new Intent(); - intent.setSourceBounds(rect); - handler.onClickHandler(v, pendingIntent, intent); - } - }; - } - target.setTagInternal(R.id.pending_intent_tag, pendingIntent); - target.setOnClickListener(listener); + target.setOnClickListener(v -> mResponse.handleViewClick(v, handler)); } @Override public int getActionTag() { - return SET_ON_CLICK_PENDING_INTENT_TAG; + return SET_ON_CLICK_RESPONSE_TAG; } - @UnsupportedAppUsage - PendingIntent pendingIntent; + final RemoteResponse mResponse; } - private static Rect getSourceBounds(View v) { + /** @hide **/ + public static Rect getSourceBounds(View v) { final float appScale = v.getContext().getResources() .getCompatibilityInfo().applicationScale; final int[] pos = new int[2]; @@ -2413,8 +2295,8 @@ public class RemoteViews implements Parcelable, Filter { private Action getActionFromParcel(Parcel parcel, int depth) { int tag = parcel.readInt(); switch (tag) { - case SET_ON_CLICK_PENDING_INTENT_TAG: - return new SetOnClickPendingIntent(parcel); + case SET_ON_CLICK_RESPONSE_TAG: + return new SetOnClickResponse(parcel); case SET_DRAWABLE_TINT_TAG: return new SetDrawableTint(parcel); case REFLECTION_ACTION_TAG: @@ -2430,8 +2312,6 @@ public class RemoteViews implements Parcelable, Filter { return new SetEmptyView(parcel); case SET_PENDING_INTENT_TEMPLATE_TAG: return new SetPendingIntentTemplate(parcel); - case SET_ON_CLICK_FILL_IN_INTENT_TAG: - return new SetOnClickFillInIntent(parcel); case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG: return new SetRemoteViewsAdapterIntent(parcel); case TEXT_VIEW_DRAWABLE_ACTION_TAG: @@ -2834,7 +2714,7 @@ public class RemoteViews implements Parcelable, Filter { * 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 pendingIntent may get overridden + * 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}, @@ -2846,7 +2726,19 @@ public class RemoteViews implements Parcelable, Filter { * @param pendingIntent The {@link PendingIntent} to send when user clicks */ public void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) { - addAction(new SetOnClickPendingIntent(viewId, pendingIntent)); + setOnClickResponse(viewId, RemoteResponse.fromPendingIntent(pendingIntent)); + } + + /** + * Equivalent of calling + * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} + * to launch the provided {@link RemoteResponse}. + * + * @param viewId The id of the view that will trigger the {@link RemoteResponse} when clicked + * @param response The {@link RemoteResponse} to send when user clicks + */ + public void setOnClickResponse(int viewId, RemoteResponse response) { + addAction(new SetOnClickResponse(viewId, response)); } /** @@ -2883,7 +2775,7 @@ public class RemoteViews implements Parcelable, Filter { * in order to determine the on-click behavior of the view specified by viewId */ public void setOnClickFillInIntent(int viewId, Intent fillInIntent) { - addAction(new SetOnClickFillInIntent(viewId, fillInIntent)); + setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent)); } /** @@ -3903,4 +3795,213 @@ public class RemoteViews implements Parcelable, Filter { } } } + + /** + * Class representing a response to an action performed on any element of a RemoteViews. + */ + public static class RemoteResponse { + + private PendingIntent mPendingIntent; + private Intent mFillIntent; + + private IntArray mViewIds; + private ArrayList<String> mElementNames; + + /** + * Creates a response which sends a pending intent as part of the response. The source + * bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the + * target view in screen space. + * Note that any activity options associated with the mPendingIntent may get overridden + * before starting the intent. + * + * @param pendingIntent The {@link PendingIntent} to send as part of the response + */ + public static RemoteResponse fromPendingIntent(PendingIntent pendingIntent) { + RemoteResponse response = new RemoteResponse(); + response.mPendingIntent = pendingIntent; + return response; + } + + /** + * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is + * very costly to set PendingIntents on the individual items, and is hence not permitted. + * Instead a single PendingIntent template can be set on the collection, see {@link + * RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click + * action of a given item can be distinguished by setting a fillInIntent on that item. The + * fillInIntent is then combined with the PendingIntent template in order to determine the + * final intent which will be executed when the item is clicked. This works as follows: any + * fields which are left blank in the PendingIntent template, but are provided by the + * fillInIntent will be overwritten, and the resulting PendingIntent will be used. The rest + * of the PendingIntent template will then be filled in with the associated fields that are + * set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details. + * Creates a response which sends a pending intent as part of the response. The source + * bounds ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the + * target view in screen space. + * Note that any activity options associated with the mPendingIntent may get overridden + * before starting the intent. + * + * @param fillIntent The intent which will be combined with the parent's PendingIntent in + * order to determine the behavior of the response + * + * @see RemoteViews#setPendingIntentTemplate(int, PendingIntent) + * @see RemoteViews#setOnClickFillInIntent(int, Intent) + * @return + */ + public static RemoteResponse fromFillInIntent(Intent fillIntent) { + RemoteResponse response = new RemoteResponse(); + response.mFillIntent = fillIntent; + return response; + } + + /** + * Adds a shared element to be transferred as part of the transition between Activities + * using cross-Activity scene animations. The position of the first element will be used as + * the epicenter for the exit Transition. The position of the associated shared element in + * the launched Activity will be the epicenter of its entering Transition. + * + * @param viewId The id of the view to be shared as part of the transition + * @param sharedElementName The shared element name for this view + * + * @see ActivityOptions#makeSceneTransitionAnimation(Activity, Pair[]) + */ + public RemoteResponse addSharedElement(int viewId, String sharedElementName) { + if (mViewIds == null) { + mViewIds = new IntArray(); + mElementNames = new ArrayList<>(); + } + mViewIds.add(viewId); + mElementNames.add(sharedElementName); + return this; + } + + private void writeToParcel(Parcel dest, int flags) { + PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest); + if (mPendingIntent == null) { + // Only write the intent if pending intent is null + dest.writeTypedObject(mFillIntent, flags); + } + dest.writeIntArray(mViewIds == null ? null : mViewIds.toArray()); + dest.writeStringList(mElementNames); + } + + private void readFromParcel(Parcel parcel) { + mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); + if (mPendingIntent == null) { + mFillIntent = parcel.readTypedObject(Intent.CREATOR); + } + int[] viewIds = parcel.createIntArray(); + mViewIds = viewIds == null ? null : IntArray.wrap(viewIds); + mElementNames = parcel.createStringArrayList(); + } + + private void handleViewClick(View v, OnClickHandler handler) { + final PendingIntent pi; + if (mPendingIntent != null) { + pi = mPendingIntent; + } else if (mFillIntent != null) { + // Insure that this view is a child of an AdapterView + View parent = (View) v.getParent(); + // Break the for loop on the first encounter of: + // 1) an AdapterView, + // 2) an AppWidgetHostView that is not a RemoteViewsFrameLayout, or + // 3) a null parent. + // 2) and 3) are unexpected and catch the case where a child is not + // correctly parented in an AdapterView. + while (parent != null && !(parent instanceof AdapterView<?>) + && !((parent instanceof AppWidgetHostView) + && !(parent instanceof RemoteViewsAdapter.RemoteViewsFrameLayout))) { + parent = (View) parent.getParent(); + } + + if (!(parent instanceof AdapterView<?>)) { + // Somehow they've managed to get this far without having + // and AdapterView as a parent. + Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent"); + return; + } + // Insure that a template pending intent has been set on an ancestor + if (!(parent.getTag() instanceof PendingIntent)) { + Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without" + + " calling setPendingIntentTemplate on parent."); + return; + } + + pi = (PendingIntent) parent.getTag(); + } else { + Log.e(LOG_TAG, "Response has neither pendingIntent nor fillInIntent"); + return; + } + + handler.onClickHandler(v, pi, this); + } + + /** @hide */ + public Pair<Intent, ActivityOptions> getLaunchOptions(View view) { + Intent intent = mPendingIntent != null ? new Intent() : new Intent(mFillIntent); + intent.setSourceBounds(getSourceBounds(view)); + + ActivityOptions opts = null; + + Context context = view.getContext(); + if (context.getResources().getBoolean( + com.android.internal.R.bool.config_overrideRemoteViewsActivityTransition)) { + TypedArray windowStyle = context.getTheme().obtainStyledAttributes( + com.android.internal.R.styleable.Window); + int windowAnimations = windowStyle.getResourceId( + com.android.internal.R.styleable.Window_windowAnimationStyle, 0); + TypedArray windowAnimationStyle = context.obtainStyledAttributes( + windowAnimations, com.android.internal.R.styleable.WindowAnimation); + int enterAnimationId = windowAnimationStyle.getResourceId(com.android.internal.R + .styleable.WindowAnimation_activityOpenRemoteViewsEnterAnimation, 0); + windowStyle.recycle(); + windowAnimationStyle.recycle(); + + if (enterAnimationId != 0) { + opts = ActivityOptions.makeCustomAnimation(context, + enterAnimationId, 0); + opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + } + + if (opts == null && mViewIds != null && mElementNames != null) { + View parent = (View) view.getParent(); + while (parent != null && !(parent instanceof AppWidgetHostView)) { + parent = (View) parent.getParent(); + } + if (parent instanceof AppWidgetHostView) { + opts = ((AppWidgetHostView) parent).createSharedElementActivityOptions( + mViewIds.toArray(), + mElementNames.toArray(new String[mElementNames.size()]), intent); + } + } + + if (opts == null) { + opts = ActivityOptions.makeBasic(); + opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + return Pair.create(intent, opts); + } + } + + /** @hide */ + public static boolean startPendingIntent(View view, PendingIntent pendingIntent, + Pair<Intent, ActivityOptions> options) { + try { + // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? + Context context = view.getContext(); + // The NEW_TASK flags are applied through the activity options and not as a part of + // the call to startIntentSender() to ensure that they are consistently applied to + // both mutable and immutable PendingIntents. + context.startIntentSender( + pendingIntent.getIntentSender(), options.first, + 0, 0, 0, options.second.toBundle()); + } catch (IntentSender.SendIntentException e) { + Log.e(LOG_TAG, "Cannot send pending intent: ", e); + return false; + } catch (Exception e) { + Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e); + return false; + } + return true; + } } diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 70cf097f42a3..44561227a497 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -16,11 +16,14 @@ package android.widget; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import android.app.ActivityOptions; import android.app.PendingIntent; +import android.appwidget.AppWidgetHostView; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -444,4 +447,40 @@ public class RemoteViewsTest { } return found[0]; } + + @Test + public void sharedElement_pendingIntent_notifyParent() throws Exception { + RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test); + PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, + new Intent("android.widget.RemoteViewsTest_shared_element"), + PendingIntent.FLAG_ONE_SHOT); + views.setOnClickResponse(R.id.image, RemoteViews.RemoteResponse.fromPendingIntent(pi) + .addSharedElement(0, "e0") + .addSharedElement(1, "e1") + .addSharedElement(2, "e2")); + + WidgetContainer container = new WidgetContainer(mContext); + container.addView(new RemoteViews(views).apply(mContext, container)); + container.findViewById(R.id.image).performClick(); + + assertArrayEquals(container.mSharedViewIds, new int[] {0, 1, 2}); + assertArrayEquals(container.mSharedViewNames, new String[] {"e0", "e1", "e2"}); + } + + private class WidgetContainer extends AppWidgetHostView { + int[] mSharedViewIds; + String[] mSharedViewNames; + + WidgetContainer(Context context) { + super(context); + } + + @Override + public ActivityOptions createSharedElementActivityOptions( + int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) { + mSharedViewIds = sharedViewIds; + mSharedViewNames = sharedViewNames; + return null; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index f30377ead957..8c53cc2a06a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -35,6 +35,7 @@ import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -47,7 +48,6 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; -import com.android.systemui.InitController; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -122,7 +122,7 @@ public class NotificationRemoteInputManager implements Dumpable { @Override public boolean onClickHandler( - final View view, final PendingIntent pendingIntent, final Intent fillInIntent) { + View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) { getShadeController().wakeUpIfDozing(SystemClock.uptimeMillis(), view); if (handleRemoteInput(view, pendingIntent)) { @@ -141,8 +141,12 @@ public class NotificationRemoteInputManager implements Dumpable { ActivityManager.getService().resumeAppSwitches(); } catch (RemoteException e) { } - return mCallback.handleRemoteViewClick(view, pendingIntent, fillInIntent, - () -> super.onClickHandler(view, pendingIntent, fillInIntent)); + return mCallback.handleRemoteViewClick(pendingIntent, () -> { + Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); + options.second.setLaunchWindowingMode( + WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); + return RemoteViews.startPendingIntent(view, pendingIntent, options); + }); } private void logActionClick(View view) { @@ -180,13 +184,6 @@ public class NotificationRemoteInputManager implements Dumpable { return null; } - @Override - protected ActivityOptions getActivityOptions(Context context) { - ActivityOptions options = super.getActivityOptions(context); - options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); - return options; - } - private boolean handleRemoteInput(View view, PendingIntent pendingIntent) { if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) { return true; @@ -661,14 +658,11 @@ public class NotificationRemoteInputManager implements Dumpable { * Performs any special handling for a remote view click. The default behaviour can be * called through the defaultHandler parameter. * - * @param view * @param pendingIntent - * @param fillInIntent * @param defaultHandler * @return true iff the click was handled */ - boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent, - ClickHandler defaultHandler); + boolean handleRemoteViewClick(PendingIntent pendingIntent, ClickHandler defaultHandler); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 06f9658a0902..a743d41e8d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -33,7 +33,6 @@ import android.os.RemoteException; import android.os.UserHandle; import android.view.View; import android.view.ViewParent; -import android.view.ViewTreeObserver; import com.android.systemui.Dependency; import com.android.systemui.plugins.ActivityStarter; @@ -44,7 +43,6 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationRemoteInputManager.Callback; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardMonitor; @@ -208,8 +206,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks { } @Override - public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, - Intent fillInIntent, NotificationRemoteInputManager.ClickHandler defaultHandler) { + public boolean handleRemoteViewClick(PendingIntent pendingIntent, + NotificationRemoteInputManager.ClickHandler defaultHandler) { final boolean isActivity = pendingIntent.isActivity(); if (isActivity) { final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java index fdb66cca92b2..0d2d3451b90c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java @@ -167,7 +167,7 @@ public class NotificationInflaterTest extends SysuiTestCase { CountDownLatch countDownLatch = new CountDownLatch(1); NotificationInflater.applyRemoteView(result, FLAG_CONTENT_VIEW_EXPANDED, 0, new ArrayMap() /* cachedContentViews */, mRow, false /* redactAmbient */, - true /* isNewView */, new RemoteViews.OnClickHandler(), + true /* isNewView */, (v, p, r) -> true, new NotificationInflater.InflationCallback() { @Override public void handleInflationException(StatusBarNotification notification, diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java index 742d49443549..f79f6ff73d29 100644 --- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java @@ -203,15 +203,11 @@ final class FillUi { .getInteger(com.android.internal.R.integer.autofill_max_visible_datasets); } - final RemoteViews.OnClickHandler interceptionHandler = new RemoteViews.OnClickHandler() { - @Override - public boolean onClickHandler(View view, PendingIntent pendingIntent, - Intent fillInIntent) { - if (pendingIntent != null) { - mCallback.startIntentSender(pendingIntent.getIntentSender()); - } - return true; + final RemoteViews.OnClickHandler interceptionHandler = (view, pendingIntent, r) -> { + if (pendingIntent != null) { + mCallback.startIntentSender(pendingIntent.getIntentSender()); } + return true; }; if (response.getAuthentication() != null) { @@ -369,13 +365,9 @@ final class FillUi { * Creates a remoteview interceptor used to block clicks. */ private RemoteViews.OnClickHandler newClickBlocker() { - return new RemoteViews.OnClickHandler() { - @Override - public boolean onClickHandler(View view, PendingIntent pendingIntent, - Intent fillInIntent) { - if (sVerbose) Slog.v(TAG, "Ignoring click on " + view); - return true; - } + return (view, pendingIntent, response) -> { + if (sVerbose) Slog.v(TAG, "Ignoring click on " + view); + return true; }; } diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 89b442ecbbda..1e30c8afffa3 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -304,39 +304,36 @@ final class SaveUi { } } - final RemoteViews.OnClickHandler handler = new RemoteViews.OnClickHandler() { - @Override - public boolean onClickHandler(View view, PendingIntent pendingIntent, - Intent intent) { - final LogMaker log = - newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, type); - // We need to hide the Save UI before launching the pending intent, and - // restore back it once the activity is finished, and that's achieved by - // adding a custom extra in the activity intent. - final boolean isValid = isValidLink(pendingIntent, intent); - if (!isValid) { - log.setType(MetricsEvent.TYPE_UNKNOWN); - mMetricsLogger.write(log); - return false; - } - if (sVerbose) Slog.v(TAG, "Intercepting custom description intent"); - final IBinder token = mPendingUi.getToken(); - intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); - try { - mPendingUi.client.startIntentSender(pendingIntent.getIntentSender(), - intent); - mPendingUi.setState(PendingUi.STATE_PENDING); - if (sDebug) Slog.d(TAG, "hiding UI until restored with token " + token); - hide(); - log.setType(MetricsEvent.TYPE_OPEN); - mMetricsLogger.write(log); - return true; - } catch (RemoteException e) { - Slog.w(TAG, "error triggering pending intent: " + intent); - log.setType(MetricsEvent.TYPE_FAILURE); - mMetricsLogger.write(log); - return false; - } + final RemoteViews.OnClickHandler handler = (view, pendingIntent, response) -> { + Intent intent = response.getLaunchOptions(view).first; + final LogMaker log = + newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, type); + // We need to hide the Save UI before launching the pending intent, and + // restore back it once the activity is finished, and that's achieved by + // adding a custom extra in the activity intent. + final boolean isValid = isValidLink(pendingIntent, intent); + if (!isValid) { + log.setType(MetricsEvent.TYPE_UNKNOWN); + mMetricsLogger.write(log); + return false; + } + if (sVerbose) Slog.v(TAG, "Intercepting custom description intent"); + final IBinder token = mPendingUi.getToken(); + intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); + try { + mPendingUi.client.startIntentSender(pendingIntent.getIntentSender(), + intent); + mPendingUi.setState(PendingUi.STATE_PENDING); + if (sDebug) Slog.d(TAG, "hiding UI until restored with token " + token); + hide(); + log.setType(MetricsEvent.TYPE_OPEN); + mMetricsLogger.write(log); + return true; + } catch (RemoteException e) { + Slog.w(TAG, "error triggering pending intent: " + intent); + log.setType(MetricsEvent.TYPE_FAILURE); + mMetricsLogger.write(log); + return false; } }; |