1 /* 2 * Copyright (C) 2015 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.view.View.MeasureSpec.makeMeasureSpec; 20 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 21 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 22 23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY; 24 import static com.android.launcher3.Utilities.ATLEAST_S; 25 26 import android.content.Context; 27 import android.graphics.Bitmap; 28 import android.graphics.drawable.Drawable; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.util.Size; 32 import android.view.Gravity; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewPropertyAnimator; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.widget.FrameLayout; 39 import android.widget.ImageView; 40 import android.widget.LinearLayout; 41 import android.widget.RemoteViews; 42 import android.widget.TextView; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 47 import com.android.launcher3.CheckLongPressHelper; 48 import com.android.launcher3.DeviceProfile; 49 import com.android.launcher3.Launcher; 50 import com.android.launcher3.R; 51 import com.android.launcher3.icons.BaseIconFactory; 52 import com.android.launcher3.icons.FastBitmapDrawable; 53 import com.android.launcher3.icons.RoundDrawableWrapper; 54 import com.android.launcher3.icons.cache.HandlerRunnable; 55 import com.android.launcher3.model.WidgetItem; 56 import com.android.launcher3.views.ActivityContext; 57 import com.android.launcher3.widget.util.WidgetSizes; 58 59 import java.util.function.Consumer; 60 61 /** 62 * Represents the individual cell of the widget inside the widget tray. The preview is drawn 63 * horizontally centered, and scaled down if needed. 64 * 65 * This view does not support padding. Since the image is scaled down to fit the view, padding will 66 * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth 67 * transition from the view to drag view, so when adding padding support, DnD would need to 68 * consider the appropriate scaling factor. 69 */ 70 public class WidgetCell extends LinearLayout { 71 72 private static final String TAG = "WidgetCell"; 73 private static final boolean DEBUG = false; 74 75 private static final int FADE_IN_DURATION_MS = 90; 76 77 /** Widget cell width is calculated by multiplying this factor to grid cell width. */ 78 private static final float WIDTH_SCALE = 3f; 79 80 /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ 81 private static final float PREVIEW_SCALE = 0.8f; 82 83 /** 84 * The maximum dimension that can be used as the size in 85 * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int)}. 86 * 87 * <p>This is equal to (1 << MeasureSpec.MODE_SHIFT) - 1. 88 */ 89 private static final int MAX_MEASURE_SPEC_DIMENSION = (1 << 30) - 1; 90 91 /** 92 * The target preview width, in pixels, of a widget or a shortcut. 93 * 94 * <p>The actual preview width may be smaller than or equal to this value subjected to scaling. 95 */ 96 protected int mTargetPreviewWidth; 97 98 /** 99 * The target preview height, in pixels, of a widget or a shortcut. 100 * 101 * <p>The actual preview height may be smaller than or equal to this value subjected to scaling. 102 */ 103 protected int mTargetPreviewHeight; 104 105 protected int mPresetPreviewSize; 106 107 private int mCellSize; 108 109 /** 110 * The scale of the preview container. 111 */ 112 private float mPreviewContainerScale = 1f; 113 114 private FrameLayout mWidgetImageContainer; 115 private WidgetImageView mWidgetImage; 116 private ImageView mWidgetBadge; 117 private TextView mWidgetName; 118 private TextView mWidgetDims; 119 private TextView mWidgetDescription; 120 121 protected WidgetItem mItem; 122 123 private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader; 124 125 protected HandlerRunnable mActiveRequest; 126 private boolean mAnimatePreview = true; 127 128 protected final ActivityContext mActivity; 129 private final CheckLongPressHelper mLongPressHelper; 130 private final float mEnforcedCornerRadius; 131 132 private RemoteViews mRemoteViewsPreview; 133 private NavigableAppWidgetHostView mAppWidgetHostViewPreview; 134 private float mAppWidgetHostViewScale = 1f; 135 private int mSourceContainer = CONTAINER_WIDGETS_TRAY; 136 WidgetCell(Context context)137 public WidgetCell(Context context) { 138 this(context, null); 139 } 140 WidgetCell(Context context, AttributeSet attrs)141 public WidgetCell(Context context, AttributeSet attrs) { 142 this(context, attrs, 0); 143 } 144 WidgetCell(Context context, AttributeSet attrs, int defStyle)145 public WidgetCell(Context context, AttributeSet attrs, int defStyle) { 146 super(context, attrs, defStyle); 147 148 mActivity = ActivityContext.lookupContext(context); 149 mWidgetPreviewLoader = new DatabaseWidgetPreviewLoader(context); 150 mLongPressHelper = new CheckLongPressHelper(this); 151 mLongPressHelper.setLongPressTimeoutFactor(1); 152 153 setContainerWidth(); 154 setWillNotDraw(false); 155 setClipToPadding(false); 156 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 157 mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context); 158 } 159 setContainerWidth()160 private void setContainerWidth() { 161 mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE); 162 mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE); 163 mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize; 164 } 165 166 @Override onFinishInflate()167 protected void onFinishInflate() { 168 super.onFinishInflate(); 169 170 mWidgetImageContainer = findViewById(R.id.widget_preview_container); 171 mWidgetImage = findViewById(R.id.widget_preview); 172 mWidgetBadge = findViewById(R.id.widget_badge); 173 mWidgetName = findViewById(R.id.widget_name); 174 mWidgetDims = findViewById(R.id.widget_dims); 175 mWidgetDescription = findViewById(R.id.widget_description); 176 } 177 setRemoteViewsPreview(RemoteViews view)178 public void setRemoteViewsPreview(RemoteViews view) { 179 mRemoteViewsPreview = view; 180 } 181 182 @Nullable getRemoteViewsPreview()183 public RemoteViews getRemoteViewsPreview() { 184 return mRemoteViewsPreview; 185 } 186 187 /** Returns the app widget host view scale, which is a value between [0f, 1f]. */ getAppWidgetHostViewScale()188 public float getAppWidgetHostViewScale() { 189 return mAppWidgetHostViewScale; 190 } 191 192 /** 193 * Called to clear the view and free attached resources. (e.g., {@link Bitmap} 194 */ clear()195 public void clear() { 196 if (DEBUG) { 197 Log.d(TAG, "reset called on:" + mWidgetName.getText()); 198 } 199 mWidgetImage.animate().cancel(); 200 mWidgetImage.setDrawable(null); 201 mWidgetImage.setVisibility(View.VISIBLE); 202 mWidgetBadge.setImageDrawable(null); 203 mWidgetBadge.setVisibility(View.GONE); 204 mWidgetName.setText(null); 205 mWidgetDims.setText(null); 206 mWidgetDescription.setText(null); 207 mWidgetDescription.setVisibility(GONE); 208 mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize; 209 210 if (mActiveRequest != null) { 211 mActiveRequest.cancel(); 212 mActiveRequest = null; 213 } 214 mRemoteViewsPreview = null; 215 if (mAppWidgetHostViewPreview != null) { 216 mWidgetImageContainer.removeView(mAppWidgetHostViewPreview); 217 } 218 mAppWidgetHostViewPreview = null; 219 mAppWidgetHostViewScale = 1f; 220 mItem = null; 221 } 222 setSourceContainer(int sourceContainer)223 public void setSourceContainer(int sourceContainer) { 224 this.mSourceContainer = sourceContainer; 225 } 226 227 /** 228 * Applies the item to this view 229 */ applyFromCellItem(WidgetItem item)230 public void applyFromCellItem(WidgetItem item) { 231 applyFromCellItem(item, 1f); 232 } 233 234 /** 235 * Applies the item to this view 236 */ applyFromCellItem(WidgetItem item, float previewScale)237 public void applyFromCellItem(WidgetItem item, float previewScale) { 238 applyFromCellItem(item, previewScale, this::applyPreview, null); 239 } 240 241 /** 242 * Applies the item to this view 243 * @param item item to apply 244 * @param previewScale factor to scale the preview 245 * @param callback callback when preview is loaded in case the preview is being loaded or cached 246 * @param cachedPreview previously cached preview bitmap is present 247 */ applyFromCellItem(WidgetItem item, float previewScale, @NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview)248 public void applyFromCellItem(WidgetItem item, float previewScale, 249 @NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) { 250 // setPreviewSize 251 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 252 Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, item); 253 mTargetPreviewWidth = widgetSize.getWidth(); 254 mTargetPreviewHeight = widgetSize.getHeight(); 255 mPreviewContainerScale = previewScale; 256 257 applyPreviewOnAppWidgetHostView(item); 258 259 Context context = getContext(); 260 mItem = item; 261 mWidgetName.setText(mItem.label); 262 mWidgetName.setContentDescription( 263 context.getString(R.string.widget_preview_context_description, mItem.label)); 264 mWidgetDims.setText(context.getString(R.string.widget_dims_format, 265 mItem.spanX, mItem.spanY)); 266 mWidgetDims.setContentDescription(context.getString( 267 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY)); 268 if (ATLEAST_S && mItem.widgetInfo != null) { 269 CharSequence description = mItem.widgetInfo.loadDescription(context); 270 if (description != null && description.length() > 0) { 271 mWidgetDescription.setText(description); 272 mWidgetDescription.setVisibility(VISIBLE); 273 } else { 274 mWidgetDescription.setVisibility(GONE); 275 } 276 } 277 278 if (item.activityInfo != null) { 279 setTag(new PendingAddShortcutInfo(item.activityInfo)); 280 } else { 281 setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer)); 282 } 283 284 ensurePreviewWithCallback(callback, cachedPreview); 285 } 286 applyPreviewOnAppWidgetHostView(WidgetItem item)287 private void applyPreviewOnAppWidgetHostView(WidgetItem item) { 288 if (mRemoteViewsPreview != null) { 289 mAppWidgetHostViewPreview = createAppWidgetHostView(getContext()); 290 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, 291 mRemoteViewsPreview); 292 return; 293 } 294 295 if (!item.hasPreviewLayout()) return; 296 297 Context context = getContext(); 298 // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview as 299 // a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, which 300 // supports applying local color extraction during drag & drop. 301 mAppWidgetHostViewPreview = isLauncherContext(context) 302 ? new LauncherAppWidgetHostView(context) 303 : createAppWidgetHostView(context); 304 LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo = 305 LauncherAppWidgetProviderInfo.fromProviderInfo(context, item.widgetInfo.clone()); 306 // A hack to force the initial layout to be the preview layout since there is no API for 307 // rendering a preview layout for work profile apps yet. For non-work profile layout, a 308 // proper solution is to use RemoteViews(PackageName, LayoutId). 309 launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout; 310 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, 311 launcherAppWidgetProviderInfo, /* remoteViews= */ null); 312 } 313 setAppWidgetHostViewPreview( NavigableAppWidgetHostView appWidgetHostViewPreview, LauncherAppWidgetProviderInfo providerInfo, @Nullable RemoteViews remoteViews)314 private void setAppWidgetHostViewPreview( 315 NavigableAppWidgetHostView appWidgetHostViewPreview, 316 LauncherAppWidgetProviderInfo providerInfo, 317 @Nullable RemoteViews remoteViews) { 318 appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 319 appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo); 320 appWidgetHostViewPreview.updateAppWidget(remoteViews); 321 } 322 getWidgetView()323 public WidgetImageView getWidgetView() { 324 return mWidgetImage; 325 } 326 327 @Nullable getAppWidgetHostViewPreview()328 public NavigableAppWidgetHostView getAppWidgetHostViewPreview() { 329 return mAppWidgetHostViewPreview; 330 } 331 setAnimatePreview(boolean shouldAnimate)332 public void setAnimatePreview(boolean shouldAnimate) { 333 mAnimatePreview = shouldAnimate; 334 } 335 applyPreview(Bitmap bitmap)336 private void applyPreview(Bitmap bitmap) { 337 if (bitmap != null) { 338 Drawable drawable = new RoundDrawableWrapper( 339 new FastBitmapDrawable(bitmap), mEnforcedCornerRadius); 340 341 // Scale down the preview size if it's wider than the cell. 342 float scale = 1f; 343 if (mTargetPreviewWidth > 0) { 344 float maxWidth = mTargetPreviewWidth; 345 float previewWidth = drawable.getIntrinsicWidth() * mPreviewContainerScale; 346 scale = Math.min(maxWidth / previewWidth, 1); 347 } 348 setContainerSize( 349 Math.round(drawable.getIntrinsicWidth() * scale * mPreviewContainerScale), 350 Math.round(drawable.getIntrinsicHeight() * scale * mPreviewContainerScale)); 351 mWidgetImage.setDrawable(drawable); 352 mWidgetImage.setVisibility(View.VISIBLE); 353 if (mAppWidgetHostViewPreview != null) { 354 removeView(mAppWidgetHostViewPreview); 355 mAppWidgetHostViewPreview = null; 356 } 357 } 358 359 if (mAnimatePreview) { 360 mWidgetImageContainer.setAlpha(0f); 361 ViewPropertyAnimator anim = mWidgetImageContainer.animate(); 362 anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); 363 } else { 364 mWidgetImageContainer.setAlpha(1f); 365 } 366 if (mActiveRequest != null) { 367 mActiveRequest.cancel(); 368 mActiveRequest = null; 369 } 370 } 371 372 /** Used to show the badge when the widget is in the recommended section 373 */ showBadge()374 public void showBadge() { 375 Drawable badge = mWidgetPreviewLoader.getBadgeForUser(mItem.user, 376 BaseIconFactory.getBadgeSizeForIconSize( 377 mActivity.getDeviceProfile().allAppsIconSizePx)); 378 if (badge == null) { 379 mWidgetBadge.setVisibility(View.GONE); 380 } else { 381 mWidgetBadge.setVisibility(View.VISIBLE); 382 mWidgetBadge.setImageDrawable(badge); 383 } 384 } 385 setContainerSize(int width, int height)386 private void setContainerSize(int width, int height) { 387 LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams(); 388 layoutParams.width = width; 389 layoutParams.height = height; 390 mWidgetImageContainer.setLayoutParams(layoutParams); 391 } 392 393 /** 394 * Ensures that the preview is already loaded or being loaded. If the preview is not loaded, 395 * it applies the provided cachedPreview. If that is null, it starts a loader and notifies the 396 * callback on successful load. 397 */ ensurePreviewWithCallback(Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview)398 private void ensurePreviewWithCallback(Consumer<Bitmap> callback, 399 @Nullable Bitmap cachedPreview) { 400 if (mAppWidgetHostViewPreview != null) { 401 int containerWidth = (int) (mTargetPreviewWidth * mPreviewContainerScale); 402 int containerHeight = (int) (mTargetPreviewHeight * mPreviewContainerScale); 403 setContainerSize(containerWidth, containerHeight); 404 if (mAppWidgetHostViewPreview.getChildCount() == 1) { 405 View widgetContent = mAppWidgetHostViewPreview.getChildAt(0); 406 ViewGroup.LayoutParams layoutParams = widgetContent.getLayoutParams(); 407 // We only scale preview if both the width & height of the outermost view group are 408 // not set to MATCH_PARENT. 409 boolean shouldScale = 410 layoutParams.width != MATCH_PARENT && layoutParams.height != MATCH_PARENT; 411 if (shouldScale) { 412 setNoClip(mWidgetImageContainer); 413 setNoClip(mAppWidgetHostViewPreview); 414 mAppWidgetHostViewScale = measureAndComputeWidgetPreviewScale(); 415 mAppWidgetHostViewPreview.setScaleToFit(mAppWidgetHostViewScale); 416 } 417 } 418 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 419 containerWidth, containerHeight, Gravity.FILL); 420 mAppWidgetHostViewPreview.setLayoutParams(params); 421 mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0); 422 mWidgetImage.setVisibility(View.GONE); 423 applyPreview(null); 424 return; 425 } 426 if (cachedPreview != null) { 427 applyPreview(cachedPreview); 428 return; 429 } 430 if (mActiveRequest != null) { 431 return; 432 } 433 mActiveRequest = mWidgetPreviewLoader.loadPreview( 434 mItem, new Size(mTargetPreviewWidth, mTargetPreviewHeight), callback); 435 } 436 437 @Override onTouchEvent(MotionEvent ev)438 public boolean onTouchEvent(MotionEvent ev) { 439 super.onTouchEvent(ev); 440 mLongPressHelper.onTouchEvent(ev); 441 return true; 442 } 443 444 @Override cancelLongPress()445 public void cancelLongPress() { 446 super.cancelLongPress(); 447 mLongPressHelper.cancelLongPress(); 448 } 449 createAppWidgetHostView(Context context)450 private static NavigableAppWidgetHostView createAppWidgetHostView(Context context) { 451 return new NavigableAppWidgetHostView(context) { 452 @Override 453 protected boolean shouldAllowDirectClick() { 454 return false; 455 } 456 }; 457 } 458 459 private static boolean isLauncherContext(Context context) { 460 return ActivityContext.lookupContext(context) instanceof Launcher; 461 } 462 463 @Override 464 public CharSequence getAccessibilityClassName() { 465 return WidgetCell.class.getName(); 466 } 467 468 @Override 469 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 470 super.onInitializeAccessibilityNodeInfo(info); 471 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); 472 } 473 474 private static void setNoClip(ViewGroup view) { 475 view.setClipChildren(false); 476 view.setClipToPadding(false); 477 } 478 479 private float measureAndComputeWidgetPreviewScale() { 480 if (mAppWidgetHostViewPreview.getChildCount() != 1) { 481 return 1f; 482 } 483 484 // Measure the largest possible width & height that the app widget wants to display. 485 mAppWidgetHostViewPreview.measure( 486 makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED), 487 makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED)); 488 if (mRemoteViewsPreview != null) { 489 // If RemoteViews contains multiple sizes, the best fit sized RemoteViews will be 490 // selected in onLayout. To work out the right measurement, let's layout and then 491 // measure again. 492 mAppWidgetHostViewPreview.layout( 493 /* left= */ 0, 494 /* top= */ 0, 495 /* right= */ mTargetPreviewWidth, 496 /* bottom= */ mTargetPreviewHeight); 497 mAppWidgetHostViewPreview.measure( 498 makeMeasureSpec(mTargetPreviewWidth, MeasureSpec.UNSPECIFIED), 499 makeMeasureSpec(mTargetPreviewHeight, MeasureSpec.UNSPECIFIED)); 500 501 } 502 View widgetContent = mAppWidgetHostViewPreview.getChildAt(0); 503 int appWidgetContentWidth = widgetContent.getMeasuredWidth(); 504 int appWidgetContentHeight = widgetContent.getMeasuredHeight(); 505 if (appWidgetContentWidth == 0 || appWidgetContentHeight == 0) { 506 return 1f; 507 } 508 509 // If the width / height of the widget content is set to wrap content, overrides the width / 510 // height with the measured dimension. This avoids incorrect measurement after scaling. 511 FrameLayout.LayoutParams layoutParam = 512 (FrameLayout.LayoutParams) widgetContent.getLayoutParams(); 513 if (layoutParam.width == WRAP_CONTENT) { 514 layoutParam.width = widgetContent.getMeasuredWidth(); 515 } 516 if (layoutParam.height == WRAP_CONTENT) { 517 layoutParam.height = widgetContent.getMeasuredHeight(); 518 } 519 widgetContent.setLayoutParams(layoutParam); 520 521 int horizontalPadding = mAppWidgetHostViewPreview.getPaddingStart() 522 + mAppWidgetHostViewPreview.getPaddingEnd(); 523 int verticalPadding = mAppWidgetHostViewPreview.getPaddingTop() 524 + mAppWidgetHostViewPreview.getPaddingBottom(); 525 return Math.min( 526 (mTargetPreviewWidth - horizontalPadding) * mPreviewContainerScale 527 / appWidgetContentWidth, 528 (mTargetPreviewHeight - verticalPadding) * mPreviewContainerScale 529 / appWidgetContentHeight); 530 } 531 } 532