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