1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.appwidget;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Activity;
22 import android.app.ActivityOptions;
23 import android.app.LoadedApk;
24 import android.compat.annotation.UnsupportedAppUsage;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.ContextWrapper;
28 import android.content.Intent;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.LauncherActivityInfo;
31 import android.content.pm.LauncherApps;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.res.Resources;
34 import android.graphics.Canvas;
35 import android.graphics.Color;
36 import android.graphics.PointF;
37 import android.graphics.Rect;
38 import android.os.Build;
39 import android.os.Bundle;
40 import android.os.CancellationSignal;
41 import android.os.Parcelable;
42 import android.util.AttributeSet;
43 import android.util.Log;
44 import android.util.Pair;
45 import android.util.SizeF;
46 import android.util.SparseArray;
47 import android.util.SparseIntArray;
48 import android.view.Gravity;
49 import android.view.LayoutInflater;
50 import android.view.View;
51 import android.view.accessibility.AccessibilityNodeInfo;
52 import android.widget.Adapter;
53 import android.widget.AdapterView;
54 import android.widget.BaseAdapter;
55 import android.widget.FrameLayout;
56 import android.widget.RemoteViews;
57 import android.widget.RemoteViews.InteractionHandler;
58 import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
59 import android.widget.TextView;
60 
61 import java.util.ArrayList;
62 import java.util.List;
63 import java.util.concurrent.Executor;
64 
65 /**
66  * Provides the glue to show AppWidget views. This class offers automatic animation
67  * between updates, and will try recycling old views for each incoming
68  * {@link RemoteViews}.
69  */
70 public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppWidgetHostListener {
71 
72     static final String TAG = "AppWidgetHostView";
73     private static final String KEY_JAILED_ARRAY = "jail";
74     private static final String KEY_INFLATION_ID = "inflation_id";
75 
76     static final boolean LOGD = false;
77 
78     static final int VIEW_MODE_NOINIT = 0;
79     static final int VIEW_MODE_CONTENT = 1;
80     static final int VIEW_MODE_ERROR = 2;
81     static final int VIEW_MODE_DEFAULT = 3;
82 
83     // Set of valid colors resources.
84     private static final int FIRST_RESOURCE_COLOR_ID = android.R.color.system_neutral1_0;
85     private static final int LAST_RESOURCE_COLOR_ID = android.R.color.system_accent3_1000;
86 
87     // When we're inflating the initialLayout for a AppWidget, we only allow
88     // views that are allowed in RemoteViews.
89     private static final LayoutInflater.Filter INFLATER_FILTER =
90             (clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
91 
92     Context mContext;
93     Context mRemoteContext;
94 
95     @UnsupportedAppUsage
96     int mAppWidgetId;
97     @UnsupportedAppUsage
98     AppWidgetProviderInfo mInfo;
99     View mView;
100     int mViewMode = VIEW_MODE_NOINIT;
101     // If true, we should not try to re-apply the RemoteViews on the next inflation.
102     boolean mColorMappingChanged = false;
103     private InteractionHandler mInteractionHandler;
104     private boolean mOnLightBackground;
105     private SizeF mCurrentSize = null;
106     private RemoteViews.ColorResources mColorResources = null;
107     // Stores the last remote views last inflated.
108     private RemoteViews mLastInflatedRemoteViews = null;
109     private long mLastInflatedRemoteViewsId = -1;
110 
111     private Executor mAsyncExecutor;
112     private CancellationSignal mLastExecutionSignal;
113     private SparseArray<Parcelable> mDelayedRestoredState;
114     private long mDelayedRestoredInflationId;
115 
116     /**
117      * Create a host view.  Uses default fade animations.
118      */
AppWidgetHostView(Context context)119     public AppWidgetHostView(Context context) {
120         this(context, android.R.anim.fade_in, android.R.anim.fade_out);
121     }
122 
123     /**
124      * @hide
125      */
AppWidgetHostView(Context context, InteractionHandler handler)126     public AppWidgetHostView(Context context, InteractionHandler handler) {
127         this(context, android.R.anim.fade_in, android.R.anim.fade_out);
128         mInteractionHandler = getHandler(handler);
129     }
130 
131     /**
132      * Create a host view. Uses specified animations when pushing
133      * {@link #updateAppWidget(RemoteViews)}.
134      *
135      * @param animationIn Resource ID of in animation to use
136      * @param animationOut Resource ID of out animation to use
137      */
138     @SuppressWarnings({"UnusedDeclaration"})
AppWidgetHostView(Context context, int animationIn, int animationOut)139     public AppWidgetHostView(Context context, int animationIn, int animationOut) {
140         super(context);
141         mContext = context;
142         // We want to segregate the view ids within AppWidgets to prevent
143         // problems when those ids collide with view ids in the AppWidgetHost.
144         setIsRootNamespace(true);
145     }
146 
147     /**
148      * Pass the given handler to RemoteViews when updating this widget. Unless this
149      * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
150      * should be made.
151      * @param handler
152      * @hide
153      */
setInteractionHandler(InteractionHandler handler)154     public void setInteractionHandler(InteractionHandler handler) {
155         mInteractionHandler = getHandler(handler);
156     }
157 
158     /**
159      * @hide
160      */
161     public static class AdapterChildHostView extends AppWidgetHostView {
162 
AdapterChildHostView(Context context)163         public AdapterChildHostView(Context context) {
164             super(context);
165         }
166 
167         @Override
getRemoteContextEnsuringCorrectCachedApkPath()168         public Context getRemoteContextEnsuringCorrectCachedApkPath() {
169             // To reduce noise in error messages
170             return null;
171         }
172     }
173 
174     /**
175      * Set the AppWidget that will be displayed by this view. This method also adds default padding
176      * to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)}
177      * and can be overridden in order to add custom padding.
178      */
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)179     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
180         mAppWidgetId = appWidgetId;
181         mInfo = info;
182 
183         // We add padding to the AppWidgetHostView if necessary
184         Rect padding = getDefaultPadding();
185         setPadding(padding.left, padding.top, padding.right, padding.bottom);
186 
187         // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for
188         // a widget, eg. for some widgets in safe mode.
189         if (info != null) {
190             String description = info.loadLabel(getContext().getPackageManager());
191             if ((info.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) {
192                 description = Resources.getSystem().getString(
193                         com.android.internal.R.string.suspended_widget_accessibility, description);
194             }
195             setContentDescription(description);
196         }
197     }
198 
199     /**
200      * As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting
201      * ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend
202      * that widget developers do not add extra padding to their widgets. This will help
203      * achieve consistency among widgets.
204      *
205      * Note: this method is only needed by developers of AppWidgetHosts. The method is provided in
206      * order for the AppWidgetHost to account for the automatic padding when computing the number
207      * of cells to allocate to a particular widget.
208      *
209      * @param context the current context
210      * @param component the component name of the widget
211      * @param padding Rect in which to place the output, if null, a new Rect will be allocated and
212      *                returned
213      * @return default padding for this widget, in pixels
214      */
getDefaultPaddingForWidget(Context context, ComponentName component, Rect padding)215     public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
216             Rect padding) {
217         return getDefaultPaddingForWidget(context, padding);
218     }
219 
getDefaultPaddingForWidget(Context context, Rect padding)220     private static Rect getDefaultPaddingForWidget(Context context, Rect padding) {
221         if (padding == null) {
222             padding = new Rect(0, 0, 0, 0);
223         } else {
224             padding.set(0, 0, 0, 0);
225         }
226         Resources r = context.getResources();
227         padding.left = r.getDimensionPixelSize(
228                 com.android.internal.R.dimen.default_app_widget_padding_left);
229         padding.right = r.getDimensionPixelSize(
230                 com.android.internal.R.dimen.default_app_widget_padding_right);
231         padding.top = r.getDimensionPixelSize(
232                 com.android.internal.R.dimen.default_app_widget_padding_top);
233         padding.bottom = r.getDimensionPixelSize(
234                 com.android.internal.R.dimen.default_app_widget_padding_bottom);
235         return padding;
236     }
237 
getDefaultPadding()238     private Rect getDefaultPadding() {
239         return getDefaultPaddingForWidget(mContext, null);
240     }
241 
getAppWidgetId()242     public int getAppWidgetId() {
243         return mAppWidgetId;
244     }
245 
getAppWidgetInfo()246     public AppWidgetProviderInfo getAppWidgetInfo() {
247         return mInfo;
248     }
249 
250     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)251     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
252         final SparseArray<Parcelable> jail = new SparseArray<>();
253         super.dispatchSaveInstanceState(jail);
254 
255         Bundle bundle = new Bundle();
256         bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail);
257         bundle.putLong(KEY_INFLATION_ID, mLastInflatedRemoteViewsId);
258         container.put(generateId(), bundle);
259         container.put(generateId(), bundle);
260     }
261 
generateId()262     private int generateId() {
263         final int id = getId();
264         return id == View.NO_ID ? mAppWidgetId : id;
265     }
266 
267     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)268     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
269         final Parcelable parcelable = container.get(generateId());
270 
271         SparseArray<Parcelable> jail = null;
272         long inflationId = -1;
273         if (parcelable instanceof Bundle) {
274             Bundle bundle = (Bundle) parcelable;
275             jail = bundle.getSparseParcelableArray(KEY_JAILED_ARRAY);
276             inflationId = bundle.getLong(KEY_INFLATION_ID, -1);
277         }
278 
279         if (jail == null) jail = new SparseArray<>();
280 
281         mDelayedRestoredState = jail;
282         mDelayedRestoredInflationId = inflationId;
283         restoreInstanceState();
284     }
285 
restoreInstanceState()286     void restoreInstanceState() {
287         long inflationId = mDelayedRestoredInflationId;
288         SparseArray<Parcelable> state = mDelayedRestoredState;
289         if (inflationId == -1 || inflationId != mLastInflatedRemoteViewsId) {
290             return; // We don't restore.
291         }
292         mDelayedRestoredInflationId = -1;
293         mDelayedRestoredState = null;
294         try  {
295             super.dispatchRestoreInstanceState(state);
296         } catch (Exception e) {
297             Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
298                     + (mInfo == null ? "null" : mInfo.provider), e);
299         }
300     }
301 
computeSizeFromLayout(int left, int top, int right, int bottom)302     private SizeF computeSizeFromLayout(int left, int top, int right, int bottom) {
303         float density = getResources().getDisplayMetrics().density;
304         return new SizeF(
305                 (right - left - getPaddingLeft() - getPaddingRight()) / density,
306                 (bottom - top - getPaddingTop() - getPaddingBottom()) / density
307         );
308     }
309 
310     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)311     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
312         try {
313             SizeF oldSize = mCurrentSize;
314             SizeF newSize = computeSizeFromLayout(left, top, right, bottom);
315             mCurrentSize = newSize;
316             if (mLastInflatedRemoteViews != null) {
317                 RemoteViews toApply = mLastInflatedRemoteViews.getRemoteViewsToApplyIfDifferent(
318                         oldSize, newSize);
319                 if (toApply != null) {
320                     applyRemoteViews(toApply, false);
321                     measureChildWithMargins(mView,
322                             MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
323                             0 /* widthUsed */,
324                             MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY),
325                             0 /* heightUsed */);
326                 }
327             }
328             super.onLayout(changed, left, top, right, bottom);
329         } catch (final RuntimeException e) {
330             Log.e(TAG, "Remote provider threw runtime exception, using error view instead.", e);
331             handleViewError();
332         }
333     }
334 
335     /**
336      * Remove bad view and replace with error message view
337      */
handleViewError()338     private void handleViewError() {
339         removeViewInLayout(mView);
340         View child = getErrorView();
341         prepareView(child);
342         addViewInLayout(child, 0, child.getLayoutParams());
343         measureChild(child, MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
344                 MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
345         child.layout(0, 0, child.getMeasuredWidth() + mPaddingLeft + mPaddingRight,
346                 child.getMeasuredHeight() + mPaddingTop + mPaddingBottom);
347         mView = child;
348         mViewMode = VIEW_MODE_ERROR;
349     }
350 
351     /**
352      * Provide guidance about the size of this widget to the AppWidgetManager. The widths and
353      * heights should correspond to the full area the AppWidgetHostView is given. Padding added by
354      * the framework will be accounted for automatically. This information gets embedded into the
355      * AppWidget options and causes a callback to the AppWidgetProvider. In addition, the list of
356      * sizes is explicitly set to an empty list.
357      * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
358      *
359      * @param newOptions The bundle of options, in addition to the size information,
360      *          can be null.
361      * @param minWidth The minimum width in dips that the widget will be displayed at.
362      * @param minHeight The maximum height in dips that the widget will be displayed at.
363      * @param maxWidth The maximum width in dips that the widget will be displayed at.
364      * @param maxHeight The maximum height in dips that the widget will be displayed at.
365      * @deprecated use {@link AppWidgetHostView#updateAppWidgetSize(Bundle, List)} instead.
366      */
367     @Deprecated
updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight)368     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
369             int maxHeight) {
370         updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false);
371     }
372 
373     /**
374      * Provide guidance about the size of this widget to the AppWidgetManager. The sizes should
375      * correspond to the full area the AppWidgetHostView is given. Padding added by the framework
376      * will be accounted for automatically.
377      *
378      * This method will update the option bundle with the list of sizes and the min/max bounds for
379      * width and height.
380      *
381      * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
382      *
383      * @param newOptions The bundle of options, in addition to the size information.
384      * @param sizes Sizes, in dips, the widget may be displayed at without calling the provider
385      *              again. Typically, this will be size of the widget in landscape and portrait.
386      *              On some foldables, this might include the size on the outer and inner screens.
387      */
updateAppWidgetSize(@onNull Bundle newOptions, @NonNull List<SizeF> sizes)388     public void updateAppWidgetSize(@NonNull Bundle newOptions, @NonNull List<SizeF> sizes) {
389         AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
390 
391         Rect padding = getDefaultPadding();
392         float density = getResources().getDisplayMetrics().density;
393 
394         float xPaddingDips = (padding.left + padding.right) / density;
395         float yPaddingDips = (padding.top + padding.bottom) / density;
396 
397         ArrayList<SizeF> paddedSizes = new ArrayList<>(sizes.size());
398         float minWidth = Float.MAX_VALUE;
399         float maxWidth = 0;
400         float minHeight = Float.MAX_VALUE;
401         float maxHeight = 0;
402         for (int i = 0; i < sizes.size(); i++) {
403             SizeF size = sizes.get(i);
404             SizeF paddedSize = new SizeF(Math.max(0.f, size.getWidth() - xPaddingDips),
405                     Math.max(0.f, size.getHeight() - yPaddingDips));
406             paddedSizes.add(paddedSize);
407             minWidth = Math.min(minWidth, paddedSize.getWidth());
408             maxWidth = Math.max(maxWidth, paddedSize.getWidth());
409             minHeight = Math.min(minHeight, paddedSize.getHeight());
410             maxHeight = Math.max(maxHeight, paddedSize.getHeight());
411         }
412         if (paddedSizes.equals(
413                 widgetManager.getAppWidgetOptions(mAppWidgetId).<SizeF>getParcelableArrayList(
414                         AppWidgetManager.OPTION_APPWIDGET_SIZES))) {
415             return;
416         }
417         Bundle options = newOptions.deepCopy();
418         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, (int) minWidth);
419         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, (int) minHeight);
420         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, (int) maxWidth);
421         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, (int) maxHeight);
422         options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes);
423         updateAppWidgetOptions(options);
424     }
425 
426     /**
427      * @hide
428      */
429     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight, boolean ignorePadding)430     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
431             int maxHeight, boolean ignorePadding) {
432         if (newOptions == null) {
433             newOptions = new Bundle();
434         }
435 
436         Rect padding = getDefaultPadding();
437         float density = getResources().getDisplayMetrics().density;
438 
439         int xPaddingDips = (int) ((padding.left + padding.right) / density);
440         int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
441 
442         int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips);
443         int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips);
444         int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips);
445         int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips);
446 
447         AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
448 
449         // We get the old options to see if the sizes have changed
450         Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId);
451         boolean needsUpdate = false;
452         if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ||
453                 newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) ||
454                 newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ||
455                 newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) {
456             needsUpdate = true;
457         }
458 
459         if (needsUpdate) {
460             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth);
461             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight);
462             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth);
463             newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight);
464             newOptions.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES,
465                     new ArrayList<PointF>());
466             updateAppWidgetOptions(newOptions);
467         }
468     }
469 
470     /**
471      * Specify some extra information for the widget provider. Causes a callback to the
472      * AppWidgetProvider.
473      * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
474      *
475      * @param options The bundle of options information.
476      */
updateAppWidgetOptions(Bundle options)477     public void updateAppWidgetOptions(Bundle options) {
478         AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
479     }
480 
481     /** {@inheritDoc} */
482     @Override
generateLayoutParams(AttributeSet attrs)483     public LayoutParams generateLayoutParams(AttributeSet attrs) {
484         // We're being asked to inflate parameters, probably by a LayoutInflater
485         // in a remote Context. To help resolve any remote references, we
486         // inflate through our last mRemoteContext when it exists.
487         final Context context = mRemoteContext != null ? mRemoteContext : mContext;
488         return new FrameLayout.LayoutParams(context, attrs);
489     }
490 
491     /**
492      * Sets an executor which can be used for asynchronously inflating. CPU intensive tasks like
493      * view inflation or loading images will be performed on the executor. The updates will still
494      * be applied on the UI thread.
495      *
496      * @param executor the executor to use or null.
497      */
setExecutor(Executor executor)498     public void setExecutor(Executor executor) {
499         if (mLastExecutionSignal != null) {
500             mLastExecutionSignal.cancel();
501             mLastExecutionSignal = null;
502         }
503 
504         mAsyncExecutor = executor;
505     }
506 
507     /**
508      * Sets whether the widget is being displayed on a light/white background and use an
509      * alternate UI if available.
510      * @see RemoteViews#setLightBackgroundLayoutId(int)
511      */
setOnLightBackground(boolean onLightBackground)512     public void setOnLightBackground(boolean onLightBackground) {
513         mOnLightBackground = onLightBackground;
514     }
515 
516     /**
517      * Update the AppWidgetProviderInfo for this view, and reset it to the
518      * initial layout.
519      *
520      * @hide
521      */
522     @Override
onUpdateProviderInfo(@ullable AppWidgetProviderInfo info)523     public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) {
524         setAppWidget(mAppWidgetId, info);
525         mViewMode = VIEW_MODE_NOINIT;
526         updateAppWidget(null);
527     }
528 
529     /**
530      * Process a set of {@link RemoteViews} coming in as an update from the
531      * AppWidget provider. Will animate into these new views as needed
532      */
533     @Override
updateAppWidget(RemoteViews remoteViews)534     public void updateAppWidget(RemoteViews remoteViews) {
535         mLastInflatedRemoteViews = remoteViews;
536         applyRemoteViews(remoteViews, true);
537     }
538 
539     /**
540      * Reapply the last inflated remote views, or the default view is none was inflated.
541      */
reapplyLastRemoteViews()542     private void reapplyLastRemoteViews() {
543         SparseArray<Parcelable> savedState = new SparseArray<>();
544         saveHierarchyState(savedState);
545         applyRemoteViews(mLastInflatedRemoteViews, true);
546         restoreHierarchyState(savedState);
547     }
548 
549     /**
550      * @hide
551      */
applyRemoteViews(@ullable RemoteViews remoteViews, boolean useAsyncIfPossible)552     protected void applyRemoteViews(@Nullable RemoteViews remoteViews, boolean useAsyncIfPossible) {
553         boolean recycled = false;
554         View content = null;
555         Exception exception = null;
556 
557         // Block state restore until the end of the apply.
558         mLastInflatedRemoteViewsId = -1;
559 
560         if (mLastExecutionSignal != null) {
561             mLastExecutionSignal.cancel();
562             mLastExecutionSignal = null;
563         }
564 
565         if (remoteViews == null) {
566             if (mViewMode == VIEW_MODE_DEFAULT) {
567                 // We've already done this -- nothing to do.
568                 return;
569             }
570             content = getDefaultView();
571             mViewMode = VIEW_MODE_DEFAULT;
572         } else {
573             // Select the remote view we are actually going to apply.
574             RemoteViews rvToApply = remoteViews.getRemoteViewsToApply(mContext, mCurrentSize);
575             if (mOnLightBackground) {
576                 rvToApply = rvToApply.getDarkTextViews();
577             }
578 
579             if (mAsyncExecutor != null && useAsyncIfPossible) {
580                 inflateAsync(rvToApply);
581                 return;
582             }
583             // Prepare a local reference to the remote Context so we're ready to
584             // inflate any requested LayoutParams.
585             mRemoteContext = getRemoteContextEnsuringCorrectCachedApkPath();
586 
587             if (!mColorMappingChanged && rvToApply.canRecycleView(mView)) {
588                 try {
589                     rvToApply.reapply(mContext, mView, mInteractionHandler, mCurrentSize,
590                             mColorResources);
591                     content = mView;
592                     mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
593                     recycled = true;
594                     if (LOGD) Log.d(TAG, "was able to recycle existing layout");
595                 } catch (RuntimeException e) {
596                     exception = e;
597                 }
598             }
599 
600             // Try normal RemoteView inflation
601             if (content == null) {
602                 try {
603                     content = rvToApply.apply(mContext, this, mInteractionHandler,
604                             mCurrentSize, mColorResources);
605                     mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
606                     if (LOGD) Log.d(TAG, "had to inflate new layout");
607                 } catch (RuntimeException e) {
608                     exception = e;
609                 }
610             }
611 
612             mViewMode = VIEW_MODE_CONTENT;
613         }
614 
615         applyContent(content, recycled, exception);
616     }
617 
applyContent(View content, boolean recycled, Exception exception)618     private void applyContent(View content, boolean recycled, Exception exception) {
619         mColorMappingChanged = false;
620         if (content == null) {
621             if (mViewMode == VIEW_MODE_ERROR) {
622                 // We've already done this -- nothing to do.
623                 return ;
624             }
625             if (exception != null) {
626                 Log.w(TAG, "Error inflating RemoteViews", exception);
627             }
628             content = getErrorView();
629             mViewMode = VIEW_MODE_ERROR;
630         }
631 
632         if (!recycled) {
633             prepareView(content);
634             addView(content);
635         }
636 
637         if (mView != content) {
638             removeView(mView);
639             mView = content;
640         }
641     }
642 
inflateAsync(@onNull RemoteViews remoteViews)643     private void inflateAsync(@NonNull RemoteViews remoteViews) {
644         // Prepare a local reference to the remote Context so we're ready to
645         // inflate any requested LayoutParams.
646         mRemoteContext = getRemoteContextEnsuringCorrectCachedApkPath();
647         int layoutId = remoteViews.getLayoutId();
648 
649         if (mLastExecutionSignal != null) {
650             mLastExecutionSignal.cancel();
651         }
652 
653         // If our stale view has been prepared to match active, and the new
654         // layout matches, try recycling it
655         if (!mColorMappingChanged && remoteViews.canRecycleView(mView)) {
656             try {
657                 mLastExecutionSignal = remoteViews.reapplyAsync(mContext,
658                         mView,
659                         mAsyncExecutor,
660                         new ViewApplyListener(remoteViews, layoutId, true),
661                         mInteractionHandler,
662                         mCurrentSize,
663                         mColorResources);
664             } catch (Exception e) {
665                 // Reapply failed. Try apply
666             }
667         }
668         if (mLastExecutionSignal == null) {
669             mLastExecutionSignal = remoteViews.applyAsync(mContext,
670                     this,
671                     mAsyncExecutor,
672                     new ViewApplyListener(remoteViews, layoutId, false),
673                     mInteractionHandler,
674                     mCurrentSize,
675                     mColorResources);
676         }
677     }
678 
679     private class ViewApplyListener implements RemoteViews.OnViewAppliedListener {
680         private final RemoteViews mViews;
681         private final boolean mIsReapply;
682         private final int mLayoutId;
683 
ViewApplyListener( RemoteViews views, int layoutId, boolean isReapply)684         ViewApplyListener(
685                 RemoteViews views,
686                 int layoutId,
687                 boolean isReapply) {
688             mViews = views;
689             mLayoutId = layoutId;
690             mIsReapply = isReapply;
691         }
692 
693         @Override
onViewApplied(View v)694         public void onViewApplied(View v) {
695             mViewMode = VIEW_MODE_CONTENT;
696 
697             applyContent(v, mIsReapply, null);
698 
699             mLastInflatedRemoteViewsId = mViews.computeUniqueId(mLastInflatedRemoteViews);
700             restoreInstanceState();
701             mLastExecutionSignal = null;
702         }
703 
704         @Override
onError(Exception e)705         public void onError(Exception e) {
706             if (mIsReapply) {
707                 // Try a fresh replay
708                 mLastExecutionSignal = mViews.applyAsync(mContext,
709                         AppWidgetHostView.this,
710                         mAsyncExecutor,
711                         new ViewApplyListener(mViews, mLayoutId, false),
712                         mInteractionHandler,
713                         mCurrentSize);
714             } else {
715                 applyContent(null, false, e);
716             }
717             mLastExecutionSignal = null;
718         }
719     }
720 
721     /**
722      * Process data-changed notifications for the specified view in the specified
723      * set of {@link RemoteViews} views.
724      *
725      * @hide
726      */
727     @Override
onViewDataChanged(int viewId)728     public void onViewDataChanged(int viewId) {
729         View v = findViewById(viewId);
730         if ((v != null) && (v instanceof AdapterView<?>)) {
731             AdapterView<?> adapterView = (AdapterView<?>) v;
732             Adapter adapter = adapterView.getAdapter();
733             if (adapter instanceof BaseAdapter) {
734                 BaseAdapter baseAdapter = (BaseAdapter) adapter;
735                 baseAdapter.notifyDataSetChanged();
736             }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
737                 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
738                 // connected to its associated service, and hence the adapter hasn't been set.
739                 // In this case, we need to defer the notify call until it has been set.
740                 ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
741             }
742         }
743     }
744 
745     /**
746      * Build a {@link Context} cloned into another package name, usually for the
747      * purposes of reading remote resources.
748      * @hide
749      */
getRemoteContextEnsuringCorrectCachedApkPath()750     protected Context getRemoteContextEnsuringCorrectCachedApkPath() {
751         try {
752             ApplicationInfo expectedAppInfo = mInfo.providerInfo.applicationInfo;
753             LoadedApk.checkAndUpdateApkPaths(expectedAppInfo);
754             // Return if cloned successfully, otherwise default
755             Context newContext = mContext.createApplicationContext(
756                     mInfo.providerInfo.applicationInfo,
757                     Context.CONTEXT_RESTRICTED);
758             if (mColorResources != null) {
759                 mColorResources.apply(newContext);
760             }
761             return newContext;
762         } catch (NameNotFoundException e) {
763             Log.e(TAG, "Package name " +  mInfo.providerInfo.packageName + " not found");
764             return mContext;
765         } catch (NullPointerException e) {
766             Log.e(TAG, "Error trying to create the remote context.", e);
767             return mContext;
768         }
769     }
770 
771     /**
772      * Prepare the given view to be shown. This might include adjusting
773      * {@link FrameLayout.LayoutParams} before inserting.
774      */
prepareView(View view)775     protected void prepareView(View view) {
776         // Take requested dimensions from child, but apply default gravity.
777         FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
778         if (requested == null) {
779             requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
780                     LayoutParams.MATCH_PARENT);
781         }
782 
783         requested.gravity = Gravity.CENTER;
784         view.setLayoutParams(requested);
785     }
786 
787     /**
788      * Inflate and return the default layout requested by AppWidget provider.
789      */
getDefaultView()790     protected View getDefaultView() {
791         if (LOGD) {
792             Log.d(TAG, "getDefaultView");
793         }
794         View defaultView = null;
795         Exception exception = null;
796 
797         try {
798             if (mInfo != null) {
799                 Context theirContext = getRemoteContextEnsuringCorrectCachedApkPath();
800                 mRemoteContext = theirContext;
801                 LayoutInflater inflater = (LayoutInflater)
802                         theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
803                 inflater = inflater.cloneInContext(theirContext);
804                 inflater.setFilter(INFLATER_FILTER);
805                 AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
806                 Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
807 
808                 int layoutId = mInfo.initialLayout;
809                 if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
810                     int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
811                     if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
812                         int kgLayoutId = mInfo.initialKeyguardLayout;
813                         // If a default keyguard layout is not specified, use the standard
814                         // default layout.
815                         layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
816                     }
817                 }
818                 defaultView = inflater.inflate(layoutId, this, false);
819                 if (!(defaultView instanceof AdapterView)) {
820                     // AdapterView does not support onClickListener
821                     defaultView.setOnClickListener(this::onDefaultViewClicked);
822                 }
823             } else {
824                 Log.w(TAG, "can't inflate defaultView because mInfo is missing");
825             }
826         } catch (RuntimeException e) {
827             exception = e;
828         }
829 
830         if (exception != null) {
831             Log.w(TAG, "Error inflating AppWidget " + mInfo, exception);
832         }
833 
834         if (defaultView == null) {
835             if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
836             defaultView = getErrorView();
837         }
838 
839         return defaultView;
840     }
841 
onDefaultViewClicked(View view)842     private void onDefaultViewClicked(View view) {
843         if (mInfo != null) {
844             LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class);
845             List<LauncherActivityInfo> activities = launcherApps.getActivityList(
846                     mInfo.provider.getPackageName(), mInfo.getProfile());
847             if (!activities.isEmpty()) {
848                 LauncherActivityInfo ai = activities.get(0);
849                 launcherApps.startMainActivity(ai.getComponentName(), ai.getUser(),
850                         RemoteViews.getSourceBounds(view), null);
851             }
852         }
853     }
854 
855     /**
856      * Inflate and return a view that represents an error state.
857      */
getErrorView()858     protected View getErrorView() {
859         TextView tv = new TextView(mContext);
860         tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
861         // TODO: get this color from somewhere.
862         tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
863         return tv;
864     }
865 
866     /** @hide */
867     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)868     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
869         super.onInitializeAccessibilityNodeInfoInternal(info);
870         info.setClassName(AppWidgetHostView.class.getName());
871     }
872 
873     /** @hide */
createSharedElementActivityOptions( int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent)874     public ActivityOptions createSharedElementActivityOptions(
875             int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) {
876         Context parentContext = getContext();
877         while ((parentContext instanceof ContextWrapper)
878                 && !(parentContext instanceof Activity)) {
879             parentContext = ((ContextWrapper) parentContext).getBaseContext();
880         }
881         if (!(parentContext instanceof Activity)) {
882             return null;
883         }
884 
885         List<Pair<View, String>> sharedElements = new ArrayList<>();
886         Bundle extras = new Bundle();
887 
888         for (int i = 0; i < sharedViewIds.length; i++) {
889             View view = findViewById(sharedViewIds[i]);
890             if (view != null) {
891                 sharedElements.add(Pair.create(view, sharedViewNames[i]));
892 
893                 extras.putParcelable(sharedViewNames[i], RemoteViews.getSourceBounds(view));
894             }
895         }
896 
897         if (!sharedElements.isEmpty()) {
898             fillInIntent.putExtra(RemoteViews.EXTRA_SHARED_ELEMENT_BOUNDS, extras);
899             final ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation(
900                     (Activity) parentContext,
901                     sharedElements.toArray(new Pair[sharedElements.size()]));
902             opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
903             return opts;
904         }
905         return null;
906     }
907 
getHandler(InteractionHandler handler)908     private InteractionHandler getHandler(InteractionHandler handler) {
909         return (view, pendingIntent, response) -> {
910             AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
911             if (manager != null) {
912                 manager.noteAppWidgetTapped(mAppWidgetId);
913             }
914             if (handler != null) {
915                 return handler.onInteraction(view, pendingIntent, response);
916             } else {
917                 return RemoteViews.startPendingIntent(view, pendingIntent,
918                         response.getLaunchOptions(view));
919             }
920         };
921     }
922 
923     /**
924      * Set the dynamically overloaded color resources.
925      *
926      * {@code colorMapping} maps a predefined set of color resources to their ARGB
927      * representation. Any entry not in the predefined set of colors will be ignored.
928      *
929      * Calling this method will trigger a full re-inflation of the App Widget.
930      *
931      * The color resources that can be overloaded are the ones whose name is prefixed with
932      * {@code system_neutral} or {@code system_accent}, for example
933      * {@link android.R.color#system_neutral1_500}.
934      */
setColorResources(@onNull SparseIntArray colorMapping)935     public void setColorResources(@NonNull SparseIntArray colorMapping) {
936         if (mColorResources != null
937                 && isSameColorMapping(mColorResources.getColorMapping(), colorMapping)) {
938             return;
939         }
940         setColorResources(RemoteViews.ColorResources.create(mContext, colorMapping));
941     }
942 
setColorResourcesStates(RemoteViews.ColorResources colorResources)943     private void setColorResourcesStates(RemoteViews.ColorResources colorResources) {
944         mColorResources = colorResources;
945         mColorMappingChanged = true;
946         mViewMode = VIEW_MODE_NOINIT;
947     }
948 
949     /** @hide **/
setColorResources(RemoteViews.ColorResources colorResources)950     public void setColorResources(RemoteViews.ColorResources colorResources) {
951         if (colorResources == mColorResources) {
952             return;
953         }
954         setColorResourcesStates(colorResources);
955         reapplyLastRemoteViews();
956     }
957 
958     /**
959      * @hide
960      */
setColorResourcesNoReapply(RemoteViews.ColorResources colorResources)961     public void setColorResourcesNoReapply(RemoteViews.ColorResources colorResources) {
962         if (colorResources == mColorResources) {
963             return;
964         }
965         setColorResourcesStates(colorResources);
966     }
967 
968     /** Check if, in the current context, the two color mappings are equivalent. */
isSameColorMapping(SparseIntArray oldColors, SparseIntArray newColors)969     private boolean isSameColorMapping(SparseIntArray oldColors, SparseIntArray newColors) {
970         if (oldColors.size() != newColors.size()) {
971             return false;
972         }
973         for (int i = 0; i < oldColors.size(); i++) {
974             if (oldColors.keyAt(i) != newColors.keyAt(i)
975                     || oldColors.valueAt(i) != newColors.valueAt(i)) {
976                 return false;
977             }
978         }
979         return true;
980     }
981 
982     /**
983      * Reset the dynamically overloaded resources, reverting to the default values for
984      * all the colors.
985      *
986      * If colors were defined before, calling this method will trigger a full re-inflation of the
987      * App Widget.
988      */
resetColorResources()989     public void resetColorResources() {
990         if (mColorResources != null) {
991             mColorResources = null;
992             mColorMappingChanged = true;
993             mViewMode = VIEW_MODE_NOINIT;
994             reapplyLastRemoteViews();
995         }
996     }
997 
998     @Override
dispatchDraw(@onNull Canvas canvas)999     protected void dispatchDraw(@NonNull Canvas canvas) {
1000         try {
1001             super.dispatchDraw(canvas);
1002         } catch (Exception e) {
1003             // Catch draw exceptions that may be caused by RemoteViews
1004             Log.e(TAG, "Drawing view failed: " + e);
1005             post(this::handleViewError);
1006         }
1007     }
1008 }
1009