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