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
17package android.appwidget;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager.NameNotFoundException;
23import android.content.res.Resources;
24import android.graphics.Color;
25import android.graphics.Rect;
26import android.os.Build;
27import android.os.Bundle;
28import android.os.CancellationSignal;
29import android.os.Parcelable;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.util.SparseArray;
33import android.view.Gravity;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.accessibility.AccessibilityNodeInfo;
37import android.widget.Adapter;
38import android.widget.AdapterView;
39import android.widget.BaseAdapter;
40import android.widget.FrameLayout;
41import android.widget.RemoteViews;
42import android.widget.RemoteViews.OnClickHandler;
43import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
44import android.widget.TextView;
45
46import java.util.concurrent.Executor;
47
48/**
49 * Provides the glue to show AppWidget views. This class offers automatic animation
50 * between updates, and will try recycling old views for each incoming
51 * {@link RemoteViews}.
52 */
53public class AppWidgetHostView extends FrameLayout {
54
55    static final String TAG = "AppWidgetHostView";
56    private static final String KEY_JAILED_ARRAY = "jail";
57
58    static final boolean LOGD = false;
59
60    static final int VIEW_MODE_NOINIT = 0;
61    static final int VIEW_MODE_CONTENT = 1;
62    static final int VIEW_MODE_ERROR = 2;
63    static final int VIEW_MODE_DEFAULT = 3;
64
65    // When we're inflating the initialLayout for a AppWidget, we only allow
66    // views that are allowed in RemoteViews.
67    private static final LayoutInflater.Filter INFLATER_FILTER =
68            (clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class);
69
70    Context mContext;
71    Context mRemoteContext;
72
73    int mAppWidgetId;
74    AppWidgetProviderInfo mInfo;
75    View mView;
76    int mViewMode = VIEW_MODE_NOINIT;
77    int mLayoutId = -1;
78    private OnClickHandler mOnClickHandler;
79
80    private Executor mAsyncExecutor;
81    private CancellationSignal mLastExecutionSignal;
82
83    /**
84     * Create a host view.  Uses default fade animations.
85     */
86    public AppWidgetHostView(Context context) {
87        this(context, android.R.anim.fade_in, android.R.anim.fade_out);
88    }
89
90    /**
91     * @hide
92     */
93    public AppWidgetHostView(Context context, OnClickHandler handler) {
94        this(context, android.R.anim.fade_in, android.R.anim.fade_out);
95        mOnClickHandler = handler;
96    }
97
98    /**
99     * Create a host view. Uses specified animations when pushing
100     * {@link #updateAppWidget(RemoteViews)}.
101     *
102     * @param animationIn Resource ID of in animation to use
103     * @param animationOut Resource ID of out animation to use
104     */
105    @SuppressWarnings({"UnusedDeclaration"})
106    public AppWidgetHostView(Context context, int animationIn, int animationOut) {
107        super(context);
108        mContext = context;
109        // We want to segregate the view ids within AppWidgets to prevent
110        // problems when those ids collide with view ids in the AppWidgetHost.
111        setIsRootNamespace(true);
112    }
113
114    /**
115     * Pass the given handler to RemoteViews when updating this widget. Unless this
116     * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
117     * should be made.
118     * @param handler
119     * @hide
120     */
121    public void setOnClickHandler(OnClickHandler handler) {
122        mOnClickHandler = handler;
123    }
124
125    /**
126     * Set the AppWidget that will be displayed by this view. This method also adds default padding
127     * to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)}
128     * and can be overridden in order to add custom padding.
129     */
130    public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
131        mAppWidgetId = appWidgetId;
132        mInfo = info;
133
134        // We add padding to the AppWidgetHostView if necessary
135        Rect padding = getDefaultPadding();
136        setPadding(padding.left, padding.top, padding.right, padding.bottom);
137
138        // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for
139        // a widget, eg. for some widgets in safe mode.
140        if (info != null) {
141            String description = info.loadLabel(getContext().getPackageManager());
142            if ((info.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) {
143                description = Resources.getSystem().getString(
144                        com.android.internal.R.string.suspended_widget_accessibility, description);
145            }
146            setContentDescription(description);
147        }
148    }
149
150    /**
151     * As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting
152     * ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend
153     * that widget developers do not add extra padding to their widgets. This will help
154     * achieve consistency among widgets.
155     *
156     * Note: this method is only needed by developers of AppWidgetHosts. The method is provided in
157     * order for the AppWidgetHost to account for the automatic padding when computing the number
158     * of cells to allocate to a particular widget.
159     *
160     * @param context the current context
161     * @param component the component name of the widget
162     * @param padding Rect in which to place the output, if null, a new Rect will be allocated and
163     *                returned
164     * @return default padding for this widget, in pixels
165     */
166    public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
167            Rect padding) {
168        ApplicationInfo appInfo = null;
169        try {
170            appInfo = context.getPackageManager().getApplicationInfo(component.getPackageName(), 0);
171        } catch (NameNotFoundException e) {
172            // if we can't find the package, ignore
173        }
174        return getDefaultPaddingForWidget(context, appInfo, padding);
175    }
176
177    private static Rect getDefaultPaddingForWidget(Context context, ApplicationInfo appInfo,
178            Rect padding) {
179        if (padding == null) {
180            padding = new Rect(0, 0, 0, 0);
181        } else {
182            padding.set(0, 0, 0, 0);
183        }
184        if (appInfo != null && appInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
185            Resources r = context.getResources();
186            padding.left = r.getDimensionPixelSize(com.android.internal.
187                    R.dimen.default_app_widget_padding_left);
188            padding.right = r.getDimensionPixelSize(com.android.internal.
189                    R.dimen.default_app_widget_padding_right);
190            padding.top = r.getDimensionPixelSize(com.android.internal.
191                    R.dimen.default_app_widget_padding_top);
192            padding.bottom = r.getDimensionPixelSize(com.android.internal.
193                    R.dimen.default_app_widget_padding_bottom);
194        }
195        return padding;
196    }
197
198    private Rect getDefaultPadding() {
199        return getDefaultPaddingForWidget(mContext,
200                mInfo == null ? null : mInfo.providerInfo.applicationInfo, null);
201    }
202
203    public int getAppWidgetId() {
204        return mAppWidgetId;
205    }
206
207    public AppWidgetProviderInfo getAppWidgetInfo() {
208        return mInfo;
209    }
210
211    @Override
212    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
213        final SparseArray<Parcelable> jail = new SparseArray<>();
214        super.dispatchSaveInstanceState(jail);
215
216        Bundle bundle = new Bundle();
217        bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail);
218        container.put(generateId(), bundle);
219    }
220
221    private int generateId() {
222        final int id = getId();
223        return id == View.NO_ID ? mAppWidgetId : id;
224    }
225
226    @Override
227    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
228        final Parcelable parcelable = container.get(generateId());
229
230        SparseArray<Parcelable> jail = null;
231        if (parcelable instanceof Bundle) {
232            jail = ((Bundle) parcelable).getSparseParcelableArray(KEY_JAILED_ARRAY);
233        }
234
235        if (jail == null) jail = new SparseArray<>();
236
237        try  {
238            super.dispatchRestoreInstanceState(jail);
239        } catch (Exception e) {
240            Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
241                    + (mInfo == null ? "null" : mInfo.provider), e);
242        }
243    }
244
245    @Override
246    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
247        try {
248            super.onLayout(changed, left, top, right, bottom);
249        } catch (final RuntimeException e) {
250            Log.e(TAG, "Remote provider threw runtime exception, using error view instead.", e);
251            removeViewInLayout(mView);
252            View child = getErrorView();
253            prepareView(child);
254            addViewInLayout(child, 0, child.getLayoutParams());
255            measureChild(child, MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
256                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
257            child.layout(0, 0, child.getMeasuredWidth() + mPaddingLeft + mPaddingRight,
258                    child.getMeasuredHeight() + mPaddingTop + mPaddingBottom);
259            mView = child;
260            mViewMode = VIEW_MODE_ERROR;
261        }
262    }
263
264    /**
265     * Provide guidance about the size of this widget to the AppWidgetManager. The widths and
266     * heights should correspond to the full area the AppWidgetHostView is given. Padding added by
267     * the framework will be accounted for automatically. This information gets embedded into the
268     * AppWidget options and causes a callback to the AppWidgetProvider.
269     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
270     *
271     * @param newOptions The bundle of options, in addition to the size information,
272     *          can be null.
273     * @param minWidth The minimum width in dips that the widget will be displayed at.
274     * @param minHeight The maximum height in dips that the widget will be displayed at.
275     * @param maxWidth The maximum width in dips that the widget will be displayed at.
276     * @param maxHeight The maximum height in dips that the widget will be displayed at.
277     *
278     */
279    public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
280            int maxHeight) {
281        updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false);
282    }
283
284    /**
285     * @hide
286     */
287    public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
288            int maxHeight, boolean ignorePadding) {
289        if (newOptions == null) {
290            newOptions = new Bundle();
291        }
292
293        Rect padding = getDefaultPadding();
294        float density = getResources().getDisplayMetrics().density;
295
296        int xPaddingDips = (int) ((padding.left + padding.right) / density);
297        int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
298
299        int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips);
300        int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips);
301        int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips);
302        int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips);
303
304        AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
305
306        // We get the old options to see if the sizes have changed
307        Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId);
308        boolean needsUpdate = false;
309        if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ||
310                newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) ||
311                newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ||
312                newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) {
313            needsUpdate = true;
314        }
315
316        if (needsUpdate) {
317            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth);
318            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight);
319            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth);
320            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight);
321            updateAppWidgetOptions(newOptions);
322        }
323    }
324
325    /**
326     * Specify some extra information for the widget provider. Causes a callback to the
327     * AppWidgetProvider.
328     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
329     *
330     * @param options The bundle of options information.
331     */
332    public void updateAppWidgetOptions(Bundle options) {
333        AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
334    }
335
336    /** {@inheritDoc} */
337    @Override
338    public LayoutParams generateLayoutParams(AttributeSet attrs) {
339        // We're being asked to inflate parameters, probably by a LayoutInflater
340        // in a remote Context. To help resolve any remote references, we
341        // inflate through our last mRemoteContext when it exists.
342        final Context context = mRemoteContext != null ? mRemoteContext : mContext;
343        return new FrameLayout.LayoutParams(context, attrs);
344    }
345
346    /**
347     * Sets an executor which can be used for asynchronously inflating. CPU intensive tasks like
348     * view inflation or loading images will be performed on the executor. The updates will still
349     * be applied on the UI thread.
350     *
351     * @param executor the executor to use or null.
352     */
353    public void setExecutor(Executor executor) {
354        if (mLastExecutionSignal != null) {
355            mLastExecutionSignal.cancel();
356            mLastExecutionSignal = null;
357        }
358
359        mAsyncExecutor = executor;
360    }
361
362    /**
363     * Update the AppWidgetProviderInfo for this view, and reset it to the
364     * initial layout.
365     */
366    void resetAppWidget(AppWidgetProviderInfo info) {
367        setAppWidget(mAppWidgetId, info);
368        mViewMode = VIEW_MODE_NOINIT;
369        updateAppWidget(null);
370    }
371
372    /**
373     * Process a set of {@link RemoteViews} coming in as an update from the
374     * AppWidget provider. Will animate into these new views as needed
375     */
376    public void updateAppWidget(RemoteViews remoteViews) {
377        applyRemoteViews(remoteViews, true);
378    }
379
380    /**
381     * @hide
382     */
383    protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) {
384        boolean recycled = false;
385        View content = null;
386        Exception exception = null;
387
388        if (mLastExecutionSignal != null) {
389            mLastExecutionSignal.cancel();
390            mLastExecutionSignal = null;
391        }
392
393        if (remoteViews == null) {
394            if (mViewMode == VIEW_MODE_DEFAULT) {
395                // We've already done this -- nothing to do.
396                return;
397            }
398            content = getDefaultView();
399            mLayoutId = -1;
400            mViewMode = VIEW_MODE_DEFAULT;
401        } else {
402            if (mAsyncExecutor != null && useAsyncIfPossible) {
403                inflateAsync(remoteViews);
404                return;
405            }
406            // Prepare a local reference to the remote Context so we're ready to
407            // inflate any requested LayoutParams.
408            mRemoteContext = getRemoteContext();
409            int layoutId = remoteViews.getLayoutId();
410
411            // If our stale view has been prepared to match active, and the new
412            // layout matches, try recycling it
413            if (content == null && layoutId == mLayoutId) {
414                try {
415                    remoteViews.reapply(mContext, mView, mOnClickHandler);
416                    content = mView;
417                    recycled = true;
418                    if (LOGD) Log.d(TAG, "was able to recycle existing layout");
419                } catch (RuntimeException e) {
420                    exception = e;
421                }
422            }
423
424            // Try normal RemoteView inflation
425            if (content == null) {
426                try {
427                    content = remoteViews.apply(mContext, this, mOnClickHandler);
428                    if (LOGD) Log.d(TAG, "had to inflate new layout");
429                } catch (RuntimeException e) {
430                    exception = e;
431                }
432            }
433
434            mLayoutId = layoutId;
435            mViewMode = VIEW_MODE_CONTENT;
436        }
437
438        applyContent(content, recycled, exception);
439    }
440
441    private void applyContent(View content, boolean recycled, Exception exception) {
442        if (content == null) {
443            if (mViewMode == VIEW_MODE_ERROR) {
444                // We've already done this -- nothing to do.
445                return ;
446            }
447            if (exception != null) {
448                Log.w(TAG, "Error inflating RemoteViews : " + exception.toString());
449            }
450            content = getErrorView();
451            mViewMode = VIEW_MODE_ERROR;
452        }
453
454        if (!recycled) {
455            prepareView(content);
456            addView(content);
457        }
458
459        if (mView != content) {
460            removeView(mView);
461            mView = content;
462        }
463    }
464
465    private void inflateAsync(RemoteViews remoteViews) {
466        // Prepare a local reference to the remote Context so we're ready to
467        // inflate any requested LayoutParams.
468        mRemoteContext = getRemoteContext();
469        int layoutId = remoteViews.getLayoutId();
470
471        // If our stale view has been prepared to match active, and the new
472        // layout matches, try recycling it
473        if (layoutId == mLayoutId && mView != null) {
474            try {
475                mLastExecutionSignal = remoteViews.reapplyAsync(mContext,
476                        mView,
477                        mAsyncExecutor,
478                        new ViewApplyListener(remoteViews, layoutId, true),
479                        mOnClickHandler);
480            } catch (Exception e) {
481                // Reapply failed. Try apply
482            }
483        }
484        if (mLastExecutionSignal == null) {
485            mLastExecutionSignal = remoteViews.applyAsync(mContext,
486                    this,
487                    mAsyncExecutor,
488                    new ViewApplyListener(remoteViews, layoutId, false),
489                    mOnClickHandler);
490        }
491    }
492
493    private class ViewApplyListener implements RemoteViews.OnViewAppliedListener {
494        private final RemoteViews mViews;
495        private final boolean mIsReapply;
496        private final int mLayoutId;
497
498        public ViewApplyListener(RemoteViews views, int layoutId, boolean isReapply) {
499            mViews = views;
500            mLayoutId = layoutId;
501            mIsReapply = isReapply;
502        }
503
504        @Override
505        public void onViewApplied(View v) {
506            AppWidgetHostView.this.mLayoutId = mLayoutId;
507            mViewMode = VIEW_MODE_CONTENT;
508
509            applyContent(v, mIsReapply, null);
510        }
511
512        @Override
513        public void onError(Exception e) {
514            if (mIsReapply) {
515                // Try a fresh replay
516                mLastExecutionSignal = mViews.applyAsync(mContext,
517                        AppWidgetHostView.this,
518                        mAsyncExecutor,
519                        new ViewApplyListener(mViews, mLayoutId, false),
520                        mOnClickHandler);
521            } else {
522                applyContent(null, false, e);
523            }
524        }
525    }
526
527    /**
528     * Process data-changed notifications for the specified view in the specified
529     * set of {@link RemoteViews} views.
530     */
531    void viewDataChanged(int viewId) {
532        View v = findViewById(viewId);
533        if ((v != null) && (v instanceof AdapterView<?>)) {
534            AdapterView<?> adapterView = (AdapterView<?>) v;
535            Adapter adapter = adapterView.getAdapter();
536            if (adapter instanceof BaseAdapter) {
537                BaseAdapter baseAdapter = (BaseAdapter) adapter;
538                baseAdapter.notifyDataSetChanged();
539            }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
540                // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
541                // connected to its associated service, and hence the adapter hasn't been set.
542                // In this case, we need to defer the notify call until it has been set.
543                ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
544            }
545        }
546    }
547
548    /**
549     * Build a {@link Context} cloned into another package name, usually for the
550     * purposes of reading remote resources.
551     * @hide
552     */
553    protected Context getRemoteContext() {
554        try {
555            // Return if cloned successfully, otherwise default
556            return mContext.createApplicationContext(
557                    mInfo.providerInfo.applicationInfo,
558                    Context.CONTEXT_RESTRICTED);
559        } catch (NameNotFoundException e) {
560            Log.e(TAG, "Package name " +  mInfo.providerInfo.packageName + " not found");
561            return mContext;
562        }
563    }
564
565    /**
566     * Prepare the given view to be shown. This might include adjusting
567     * {@link FrameLayout.LayoutParams} before inserting.
568     */
569    protected void prepareView(View view) {
570        // Take requested dimensions from child, but apply default gravity.
571        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
572        if (requested == null) {
573            requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
574                    LayoutParams.MATCH_PARENT);
575        }
576
577        requested.gravity = Gravity.CENTER;
578        view.setLayoutParams(requested);
579    }
580
581    /**
582     * Inflate and return the default layout requested by AppWidget provider.
583     */
584    protected View getDefaultView() {
585        if (LOGD) {
586            Log.d(TAG, "getDefaultView");
587        }
588        View defaultView = null;
589        Exception exception = null;
590
591        try {
592            if (mInfo != null) {
593                Context theirContext = getRemoteContext();
594                mRemoteContext = theirContext;
595                LayoutInflater inflater = (LayoutInflater)
596                        theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
597                inflater = inflater.cloneInContext(theirContext);
598                inflater.setFilter(INFLATER_FILTER);
599                AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
600                Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
601
602                int layoutId = mInfo.initialLayout;
603                if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
604                    int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
605                    if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
606                        int kgLayoutId = mInfo.initialKeyguardLayout;
607                        // If a default keyguard layout is not specified, use the standard
608                        // default layout.
609                        layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
610                    }
611                }
612                defaultView = inflater.inflate(layoutId, this, false);
613            } else {
614                Log.w(TAG, "can't inflate defaultView because mInfo is missing");
615            }
616        } catch (RuntimeException e) {
617            exception = e;
618        }
619
620        if (exception != null) {
621            Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
622        }
623
624        if (defaultView == null) {
625            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
626            defaultView = getErrorView();
627        }
628
629        return defaultView;
630    }
631
632    /**
633     * Inflate and return a view that represents an error state.
634     */
635    protected View getErrorView() {
636        TextView tv = new TextView(mContext);
637        tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
638        // TODO: get this color from somewhere.
639        tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
640        return tv;
641    }
642
643    /** @hide */
644    @Override
645    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
646        super.onInitializeAccessibilityNodeInfoInternal(info);
647        info.setClassName(AppWidgetHostView.class.getName());
648    }
649}
650