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