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