1 /* 2 * Copyright (C) 2014 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 com.android.launcher3.widget; 18 19 import static android.graphics.Paint.ANTI_ALIAS_FLAG; 20 import static android.graphics.Paint.DITHER_FLAG; 21 import static android.graphics.Paint.FILTER_BITMAP_FLAG; 22 23 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; 24 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 26 27 import android.appwidget.AppWidgetProviderInfo; 28 import android.content.Context; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.Matrix; 33 import android.graphics.Paint; 34 import android.graphics.PorterDuff; 35 import android.graphics.Rect; 36 import android.graphics.RectF; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.Looper; 42 import android.os.Message; 43 import android.text.Layout; 44 import android.text.StaticLayout; 45 import android.text.TextPaint; 46 import android.text.TextUtils; 47 import android.util.SizeF; 48 import android.util.TypedValue; 49 import android.view.ContextThemeWrapper; 50 import android.view.View; 51 import android.view.View.OnClickListener; 52 import android.widget.RemoteViews; 53 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 57 import com.android.launcher3.DeviceProfile; 58 import com.android.launcher3.Launcher; 59 import com.android.launcher3.LauncherAppState; 60 import com.android.launcher3.R; 61 import com.android.launcher3.icons.FastBitmapDrawable; 62 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 63 import com.android.launcher3.model.data.ItemInfoWithIcon; 64 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 65 import com.android.launcher3.model.data.PackageItemInfo; 66 import com.android.launcher3.util.RunnableList; 67 import com.android.launcher3.util.SafeCloseable; 68 import com.android.launcher3.util.Themes; 69 import com.android.launcher3.widget.ListenableAppWidgetHost.ProviderChangedListener; 70 71 import java.util.List; 72 73 public class PendingAppWidgetHostView extends LauncherAppWidgetHostView 74 implements OnClickListener, ItemInfoUpdateReceiver { 75 private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5; 76 private static final float MIN_SATURATION = 0.7f; 77 78 private static final int FLAG_DRAW_SETTINGS = 1; 79 private static final int FLAG_DRAW_ICON = 2; 80 private static final int FLAG_DRAW_LABEL = 4; 81 82 private static final int DEFERRED_ALPHA = 0x77; 83 84 private final Rect mRect = new Rect(); 85 86 private final Matrix mMatrix = new Matrix(); 87 private final RectF mPreviewBitmapRect = new RectF(); 88 private final RectF mCanvasRect = new RectF(); 89 private final Handler mHandler = new Handler(Looper.getMainLooper()); 90 private final RunnableList mOnDetachCleanup = new RunnableList(); 91 92 private final LauncherWidgetHolder mWidgetHolder; 93 private final LauncherAppWidgetProviderInfo mAppwidget; 94 private final LauncherAppWidgetInfo mInfo; 95 private final int mStartState; 96 private final boolean mDisabledForSafeMode; 97 private final CharSequence mLabel; 98 99 private OnClickListener mClickListener; 100 101 private int mDragFlags; 102 103 private Drawable mCenterDrawable; 104 private Drawable mSettingIconDrawable; 105 106 private boolean mDrawableSizeChanged; 107 private boolean mIsDeferredWidget; 108 109 private final TextPaint mPaint; 110 111 private final Paint mPreviewPaint; 112 private Layout mSetupTextLayout; 113 114 @Nullable private Bitmap mPreviewBitmap; 115 PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget)116 public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, 117 LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) { 118 this(context, widgetHolder, info, appWidget, null); 119 } 120 PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget, @Nullable Bitmap previewBitmap)121 public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, 122 LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget, 123 @Nullable Bitmap previewBitmap) { 124 this(context, widgetHolder, info, appWidget, 125 context.getResources().getText(R.string.gadget_complete_setup_text), previewBitmap); 126 super.updateAppWidget(null); 127 setOnClickListener(mActivityContext.getItemOnClickListener()); 128 129 if (info.pendingItemInfo == null) { 130 info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(), 131 info.user); 132 LauncherAppState.getInstance(context).getIconCache() 133 .updateIconInBackground(this, info.pendingItemInfo); 134 } else { 135 reapplyItemInfo(info.pendingItemInfo); 136 } 137 } 138 PendingAppWidgetHostView( Context context, LauncherWidgetHolder widgetHolder, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget)139 public PendingAppWidgetHostView( 140 Context context, LauncherWidgetHolder widgetHolder, 141 int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { 142 this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider), 143 appWidget, appWidget.label, null); 144 getBackground().mutate().setAlpha(DEFERRED_ALPHA); 145 146 mCenterDrawable = new ColorDrawable(Color.TRANSPARENT); 147 mDragFlags = FLAG_DRAW_LABEL; 148 mDrawableSizeChanged = true; 149 mIsDeferredWidget = true; 150 } 151 152 /** 153 * Set {@link Bitmap} of widget preview and update background drawable. When showing preview 154 * bitmap, we shouldn't draw background. 155 */ setPreviewBitmapAndUpdateBackground(@ullable Bitmap previewBitmap)156 public void setPreviewBitmapAndUpdateBackground(@Nullable Bitmap previewBitmap) { 157 setBackgroundResource(previewBitmap != null ? 0 : R.drawable.pending_widget_bg); 158 if (this.mPreviewBitmap == previewBitmap) { 159 return; 160 } 161 this.mPreviewBitmap = previewBitmap; 162 invalidate(); 163 } 164 PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo appwidget, CharSequence label, @Nullable Bitmap previewBitmap)165 private PendingAppWidgetHostView(Context context, 166 LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, 167 LauncherAppWidgetProviderInfo appwidget, CharSequence label, 168 @Nullable Bitmap previewBitmap) { 169 super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme)); 170 mWidgetHolder = widgetHolder; 171 mAppwidget = appwidget; 172 mInfo = info; 173 mStartState = info.restoreStatus; 174 mDisabledForSafeMode = LauncherAppState.getInstance(context).isSafeModeEnabled(); 175 mLabel = label; 176 177 mPaint = new TextPaint(); 178 mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary)); 179 mPaint.setTextSize(TypedValue.applyDimension( 180 TypedValue.COMPLEX_UNIT_PX, 181 mActivityContext.getDeviceProfile().iconTextSizePx, 182 getResources().getDisplayMetrics())); 183 mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG); 184 185 setWillNotDraw(false); 186 setPreviewBitmapAndUpdateBackground(previewBitmap); 187 } 188 189 @Override getAppWidgetInfo()190 public AppWidgetProviderInfo getAppWidgetInfo() { 191 return mAppwidget; 192 } 193 194 @Override getAppWidgetId()195 public int getAppWidgetId() { 196 return mInfo.appWidgetId; 197 } 198 199 @Override updateAppWidget(RemoteViews remoteViews)200 public void updateAppWidget(RemoteViews remoteViews) { 201 checkIfRestored(); 202 } 203 checkIfRestored()204 private void checkIfRestored() { 205 WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext()); 206 if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) { 207 MAIN_EXECUTOR.getHandler().post(this::reInflate); 208 } 209 } 210 isDeferredWidget()211 public boolean isDeferredWidget() { 212 return mIsDeferredWidget; 213 } 214 215 @Override onAttachedToWindow()216 protected void onAttachedToWindow() { 217 super.onAttachedToWindow(); 218 219 mOnDetachCleanup.executeAllAndClear(); 220 if ((mAppwidget != null) 221 && !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) 222 && mInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) { 223 // If the widget is not completely restored, but has a valid ID, then listen of 224 // updates from provider app for potential restore complete. 225 SafeCloseable updateCleanup = mWidgetHolder.addOnUpdateListener( 226 mInfo.appWidgetId, mAppwidget, this::checkIfRestored); 227 mOnDetachCleanup.add(updateCleanup::close); 228 checkIfRestored(); 229 } 230 } 231 232 @Override onDetachedFromWindow()233 protected void onDetachedFromWindow() { 234 super.onDetachedFromWindow(); 235 mOnDetachCleanup.executeAllAndClear(); 236 } 237 238 /** 239 * Forces the Launcher to reinflate the widget view 240 */ reInflate()241 public void reInflate() { 242 if (!isAttachedToWindow()) { 243 return; 244 } 245 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); 246 if (info == null) { 247 // This occurs when LauncherAppWidgetHostView is used to render a preview layout. 248 return; 249 } 250 if (mActivityContext instanceof Launcher launcher) { 251 // Remove and rebind the current widget (which was inflated in the wrong 252 // orientation), but don't delete it from the database 253 launcher.removeItem(this, info, false /* deleteFromDb */, 254 "widget removed because of configuration change"); 255 launcher.bindAppWidget(info); 256 } 257 } 258 259 @Override updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight)260 public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, 261 int maxHeight) { 262 // No-op 263 } 264 265 @Override updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes)266 public void updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes) { 267 // No-op 268 } 269 270 @Override getDefaultView()271 protected View getDefaultView() { 272 View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false); 273 defaultView.setOnClickListener(this); 274 applyState(); 275 invalidate(); 276 return defaultView; 277 } 278 279 @Override setOnClickListener(OnClickListener l)280 public void setOnClickListener(OnClickListener l) { 281 mClickListener = l; 282 } 283 isReinflateIfNeeded()284 public boolean isReinflateIfNeeded() { 285 return mStartState != mInfo.restoreStatus; 286 } 287 288 @Override onSizeChanged(int w, int h, int oldw, int oldh)289 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 290 super.onSizeChanged(w, h, oldw, oldh); 291 mDrawableSizeChanged = true; 292 } 293 294 @Override reapplyItemInfo(ItemInfoWithIcon info)295 public void reapplyItemInfo(ItemInfoWithIcon info) { 296 if (mCenterDrawable != null) { 297 mCenterDrawable.setCallback(null); 298 mCenterDrawable = null; 299 } 300 mDragFlags = FLAG_DRAW_ICON; 301 302 // The view displays three modes, 303 // 1) App icon in the center 304 // 2) Preload icon in the center 305 // 3) App icon in the center with a setup icon on the top left corner. 306 if (mDisabledForSafeMode) { 307 FastBitmapDrawable disabledIcon = info.newIcon(getContext()); 308 disabledIcon.setIsDisabled(true); 309 mCenterDrawable = disabledIcon; 310 mSettingIconDrawable = null; 311 } else if (isReadyForClickSetup()) { 312 mCenterDrawable = info.newIcon(getContext()); 313 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate(); 314 updateSettingColor(info.bitmap.color); 315 316 mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL; 317 } else { 318 mCenterDrawable = newPendingIcon(getContext(), info); 319 mSettingIconDrawable = null; 320 applyState(); 321 } 322 mCenterDrawable.setCallback(this); 323 mDrawableSizeChanged = true; 324 invalidate(); 325 } 326 updateSettingColor(int dominantColor)327 private void updateSettingColor(int dominantColor) { 328 // Make the dominant color bright. 329 float[] hsv = new float[3]; 330 Color.colorToHSV(dominantColor, hsv); 331 hsv[1] = Math.min(hsv[1], MIN_SATURATION); 332 hsv[2] = 1; 333 mSettingIconDrawable.setColorFilter(Color.HSVToColor(hsv), PorterDuff.Mode.SRC_IN); 334 } 335 336 @Override verifyDrawable(Drawable who)337 protected boolean verifyDrawable(Drawable who) { 338 return (who == mCenterDrawable) || super.verifyDrawable(who); 339 } 340 applyState()341 public void applyState() { 342 if (mCenterDrawable instanceof FastBitmapDrawable fb 343 && mInfo.pendingItemInfo != null 344 && !fb.isSameInfo(mInfo.pendingItemInfo.bitmap)) { 345 reapplyItemInfo(mInfo.pendingItemInfo); 346 } 347 if (mCenterDrawable != null) { 348 mCenterDrawable.setLevel(Math.max(mInfo.installProgress, 0)); 349 } 350 } 351 352 @Override onClick(View v)353 public void onClick(View v) { 354 // AppWidgetHostView blocks all click events on the root view. Instead handle click events 355 // on the content and pass it along. 356 if (mClickListener != null) { 357 mClickListener.onClick(this); 358 } 359 } 360 361 /** 362 * A pending widget is ready for setup after the provider is installed and 363 * 1) Widget id is not valid: the widget id is not yet bound to the provider, probably 364 * because the launcher doesn't have appropriate permissions. 365 * Note that we would still have an allocated id as that does not 366 * require any permissions and can be done during view inflation. 367 * 2) UI is not ready: the id is valid and the bound. But the widget has a configure activity 368 * which needs to be called once. 369 */ isReadyForClickSetup()370 public boolean isReadyForClickSetup() { 371 return !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) 372 && (mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) 373 || mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)); 374 } 375 updateDrawableBounds()376 private void updateDrawableBounds() { 377 DeviceProfile grid = mActivityContext.getDeviceProfile(); 378 int paddingTop = getPaddingTop(); 379 int paddingBottom = getPaddingBottom(); 380 int paddingLeft = getPaddingLeft(); 381 int paddingRight = getPaddingRight(); 382 383 int minPadding = getResources() 384 .getDimensionPixelSize(R.dimen.pending_widget_min_padding); 385 386 int availableWidth = getWidth() - paddingLeft - paddingRight - 2 * minPadding; 387 int availableHeight = getHeight() - paddingTop - paddingBottom - 2 * minPadding; 388 389 float iconSize = ((mDragFlags & FLAG_DRAW_ICON) == 0) ? 0 390 : Math.max(0, Math.min(availableWidth, availableHeight)); 391 // Use twice the setting size factor, as the setting is drawn at a corner and the 392 // icon is drawn in the center. 393 float settingIconScaleFactor = ((mDragFlags & FLAG_DRAW_SETTINGS) == 0) ? 0 394 : 1 + SETUP_ICON_SIZE_FACTOR * 2; 395 396 int maxSize = Math.max(availableWidth, availableHeight); 397 if (iconSize * settingIconScaleFactor > maxSize) { 398 // There is an overlap 399 iconSize = maxSize / settingIconScaleFactor; 400 } 401 402 int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx); 403 404 // Icon top when we do not draw the text 405 int iconTop = (getHeight() - actualIconSize) / 2; 406 mSetupTextLayout = null; 407 408 if (availableWidth > 0 && !TextUtils.isEmpty(mLabel) 409 && ((mDragFlags & FLAG_DRAW_LABEL) != 0)) { 410 // Recreate the setup text. 411 mSetupTextLayout = new StaticLayout( 412 mLabel, mPaint, availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true); 413 int textHeight = mSetupTextLayout.getHeight(); 414 415 // Extra icon size due to the setting icon 416 float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor 417 + grid.iconDrawablePaddingPx; 418 419 if (minHeightWithText < availableHeight) { 420 // We can draw the text as well 421 iconTop = (getHeight() - textHeight 422 - grid.iconDrawablePaddingPx - actualIconSize) / 2; 423 424 } else { 425 // We can't draw the text. Let the iconTop be same as before. 426 mSetupTextLayout = null; 427 } 428 } 429 430 mRect.set(0, 0, actualIconSize, actualIconSize); 431 mRect.offset((getWidth() - actualIconSize) / 2, iconTop); 432 mCenterDrawable.setBounds(mRect); 433 434 if (mSettingIconDrawable != null) { 435 mRect.left = paddingLeft + minPadding; 436 mRect.right = mRect.left + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); 437 mRect.top = paddingTop + minPadding; 438 mRect.bottom = mRect.top + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); 439 mSettingIconDrawable.setBounds(mRect); 440 } 441 442 if (mSetupTextLayout != null) { 443 // Set up position for dragging the text 444 mRect.left = paddingLeft + minPadding; 445 mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx; 446 } 447 } 448 449 @Override onDraw(Canvas canvas)450 protected void onDraw(Canvas canvas) { 451 if (mPreviewBitmap != null 452 && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) { 453 mPreviewBitmapRect.set(0, 0, mPreviewBitmap.getWidth(), mPreviewBitmap.getHeight()); 454 mCanvasRect.set(0, 0, getWidth(), getHeight()); 455 456 mMatrix.setRectToRect(mPreviewBitmapRect, mCanvasRect, Matrix.ScaleToFit.CENTER); 457 canvas.drawBitmap(mPreviewBitmap, mMatrix, mPreviewPaint); 458 return; 459 } 460 if (mCenterDrawable == null) { 461 // Nothing to draw 462 return; 463 } 464 465 if (mDrawableSizeChanged) { 466 updateDrawableBounds(); 467 mDrawableSizeChanged = false; 468 } 469 470 mCenterDrawable.draw(canvas); 471 if (mSettingIconDrawable != null) { 472 mSettingIconDrawable.draw(canvas); 473 } 474 if (mSetupTextLayout != null) { 475 canvas.save(); 476 canvas.translate(mRect.left, mRect.top); 477 mSetupTextLayout.draw(canvas); 478 canvas.restore(); 479 } 480 } 481 482 /** 483 * Creates a runnable runnable which tries to refresh the widget if it is restored 484 */ postProviderAvailabilityCheck()485 public void postProviderAvailabilityCheck() { 486 if (!mInfo.hasRestoreFlag(FLAG_PROVIDER_NOT_READY) && getAppWidgetInfo() == null) { 487 // If the info state suggests that the provider is ready, but there is no 488 // provider info attached on this pending view, recreate when the provider is available 489 DeferredWidgetRefresh restoreRunnable = new DeferredWidgetRefresh(); 490 mOnDetachCleanup.add(restoreRunnable::cleanup); 491 mHandler.post(restoreRunnable::notifyWidgetProvidersChanged); 492 } 493 } 494 495 /** 496 * Used as a workaround to ensure that the AppWidgetService receives the 497 * PACKAGE_ADDED broadcast before updating widgets. 498 * 499 * This class will periodically check for the availability of the WidgetProvider as a result 500 * of providerChanged callback from the host. When the provider is available or a timeout of 501 * 10-sec is reached, it reinflates the pending-widget which in-turn goes through the process 502 * of re-evaluating the pending state of the widget, 503 */ 504 private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener { 505 private boolean mRefreshPending = true; 506 DeferredWidgetRefresh()507 DeferredWidgetRefresh() { 508 mWidgetHolder.addProviderChangeListener(this); 509 // Force refresh after 10 seconds, if we don't get the provider changed event. 510 // This could happen when the provider is no longer available in the app. 511 Message msg = Message.obtain(getHandler(), this); 512 msg.obj = DeferredWidgetRefresh.class; 513 mHandler.sendMessageDelayed(msg, 10000); 514 } 515 516 /** 517 * Reinflate the widget if it is still attached. 518 */ 519 @Override run()520 public void run() { 521 cleanup(); 522 if (mRefreshPending) { 523 reInflate(); 524 mRefreshPending = false; 525 } 526 } 527 528 @Override notifyWidgetProvidersChanged()529 public void notifyWidgetProvidersChanged() { 530 final AppWidgetProviderInfo widgetInfo; 531 WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext()); 532 if (mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) { 533 widgetInfo = widgetHelper.findProvider(mInfo.providerName, mInfo.user); 534 } else { 535 widgetInfo = widgetHelper.getLauncherAppWidgetInfo(mInfo.appWidgetId, 536 mInfo.getTargetComponent()); 537 } 538 if (widgetInfo != null) { 539 run(); 540 } 541 } 542 543 /** 544 * Removes any scheduled callbacks and change listeners, no-op if nothing is scheduled 545 */ cleanup()546 public void cleanup() { 547 mWidgetHolder.removeProviderChangeListener(this); 548 mHandler.removeCallbacks(this); 549 } 550 } 551 } 552