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 com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY; 20 import static com.android.launcher3.Utilities.ATLEAST_S; 21 22 import android.content.Context; 23 import android.graphics.Bitmap; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.os.CancellationSignal; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.util.Size; 30 import android.view.Gravity; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.View.OnLayoutChangeListener; 34 import android.view.ViewPropertyAnimator; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 import android.widget.FrameLayout; 37 import android.widget.LinearLayout; 38 import android.widget.RemoteViews; 39 import android.widget.TextView; 40 41 import androidx.annotation.Nullable; 42 43 import com.android.launcher3.BaseActivity; 44 import com.android.launcher3.CheckLongPressHelper; 45 import com.android.launcher3.DeviceProfile; 46 import com.android.launcher3.Launcher; 47 import com.android.launcher3.R; 48 import com.android.launcher3.icons.FastBitmapDrawable; 49 import com.android.launcher3.icons.RoundDrawableWrapper; 50 import com.android.launcher3.model.WidgetItem; 51 import com.android.launcher3.widget.util.WidgetSizes; 52 53 /** 54 * Represents the individual cell of the widget inside the widget tray. The preview is drawn 55 * horizontally centered, and scaled down if needed. 56 * 57 * This view does not support padding. Since the image is scaled down to fit the view, padding will 58 * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth 59 * transition from the view to drag view, so when adding padding support, DnD would need to 60 * consider the appropriate scaling factor. 61 */ 62 public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { 63 64 private static final String TAG = "WidgetCell"; 65 private static final boolean DEBUG = false; 66 67 private static final int FADE_IN_DURATION_MS = 90; 68 69 /** Widget cell width is calculated by multiplying this factor to grid cell width. */ 70 private static final float WIDTH_SCALE = 3f; 71 72 /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ 73 private static final float PREVIEW_SCALE = 0.8f; 74 75 protected int mPreviewWidth; 76 protected int mPreviewHeight; 77 protected int mPresetPreviewSize; 78 private int mCellSize; 79 private float mPreviewScale = 1f; 80 81 private FrameLayout mWidgetImageContainer; 82 private WidgetImageView mWidgetImage; 83 private TextView mWidgetName; 84 private TextView mWidgetDims; 85 private TextView mWidgetDescription; 86 87 protected WidgetItem mItem; 88 89 private WidgetPreviewLoader mWidgetPreviewLoader; 90 91 protected CancellationSignal mActiveRequest; 92 private boolean mAnimatePreview = true; 93 94 private boolean mApplyBitmapDeferred = false; 95 private Drawable mDeferredDrawable; 96 97 protected final BaseActivity mActivity; 98 private final CheckLongPressHelper mLongPressHelper; 99 private final float mEnforcedCornerRadius; 100 private final int mShortcutPreviewPadding; 101 102 private RemoteViews mRemoteViewsPreview; 103 private NavigableAppWidgetHostView mAppWidgetHostViewPreview; 104 private int mSourceContainer = CONTAINER_WIDGETS_TRAY; 105 WidgetCell(Context context)106 public WidgetCell(Context context) { 107 this(context, null); 108 } 109 WidgetCell(Context context, AttributeSet attrs)110 public WidgetCell(Context context, AttributeSet attrs) { 111 this(context, attrs, 0); 112 } 113 WidgetCell(Context context, AttributeSet attrs, int defStyle)114 public WidgetCell(Context context, AttributeSet attrs, int defStyle) { 115 super(context, attrs, defStyle); 116 117 mActivity = BaseActivity.fromContext(context); 118 mLongPressHelper = new CheckLongPressHelper(this); 119 mLongPressHelper.setLongPressTimeoutFactor(1); 120 121 setContainerWidth(); 122 setWillNotDraw(false); 123 setClipToPadding(false); 124 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 125 mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context); 126 mShortcutPreviewPadding = 127 2 * getResources().getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); 128 } 129 setContainerWidth()130 private void setContainerWidth() { 131 mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE); 132 mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE); 133 mPreviewWidth = mPreviewHeight = mPresetPreviewSize; 134 } 135 136 @Override onFinishInflate()137 protected void onFinishInflate() { 138 super.onFinishInflate(); 139 140 mWidgetImageContainer = findViewById(R.id.widget_preview_container); 141 mWidgetImage = findViewById(R.id.widget_preview); 142 mWidgetName = findViewById(R.id.widget_name); 143 mWidgetDims = findViewById(R.id.widget_dims); 144 mWidgetDescription = findViewById(R.id.widget_description); 145 } 146 setRemoteViewsPreview(RemoteViews view)147 public void setRemoteViewsPreview(RemoteViews view) { 148 mRemoteViewsPreview = view; 149 } 150 151 @Nullable getRemoteViewsPreview()152 public RemoteViews getRemoteViewsPreview() { 153 return mRemoteViewsPreview; 154 } 155 156 /** 157 * Called to clear the view and free attached resources. (e.g., {@link Bitmap} 158 */ clear()159 public void clear() { 160 if (DEBUG) { 161 Log.d(TAG, "reset called on:" + mWidgetName.getText()); 162 } 163 mWidgetImage.animate().cancel(); 164 mWidgetImage.setDrawable(null); 165 mWidgetImage.setVisibility(View.VISIBLE); 166 mWidgetName.setText(null); 167 mWidgetDims.setText(null); 168 mWidgetDescription.setText(null); 169 mWidgetDescription.setVisibility(GONE); 170 mPreviewWidth = mPreviewHeight = mPresetPreviewSize; 171 172 if (mActiveRequest != null) { 173 mActiveRequest.cancel(); 174 mActiveRequest = null; 175 } 176 mRemoteViewsPreview = null; 177 if (mAppWidgetHostViewPreview != null) { 178 mWidgetImageContainer.removeView(mAppWidgetHostViewPreview); 179 } 180 mAppWidgetHostViewPreview = null; 181 mItem = null; 182 } 183 setSourceContainer(int sourceContainer)184 public void setSourceContainer(int sourceContainer) { 185 this.mSourceContainer = sourceContainer; 186 } 187 applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader)188 public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) { 189 applyPreviewOnAppWidgetHostView(item); 190 191 Context context = getContext(); 192 mItem = item; 193 mWidgetName.setText(mItem.label); 194 mWidgetName.setContentDescription( 195 context.getString(R.string.widget_preview_context_description, mItem.label)); 196 mWidgetDims.setText(context.getString(R.string.widget_dims_format, 197 mItem.spanX, mItem.spanY)); 198 mWidgetDims.setContentDescription(context.getString( 199 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY)); 200 if (ATLEAST_S && mItem.widgetInfo != null) { 201 CharSequence description = mItem.widgetInfo.loadDescription(context); 202 if (description != null && description.length() > 0) { 203 mWidgetDescription.setText(description); 204 mWidgetDescription.setVisibility(VISIBLE); 205 } else { 206 mWidgetDescription.setVisibility(GONE); 207 } 208 } 209 210 mWidgetPreviewLoader = loader; 211 if (item.activityInfo != null) { 212 setTag(new PendingAddShortcutInfo(item.activityInfo)); 213 } else { 214 setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer)); 215 } 216 } 217 218 applyPreviewOnAppWidgetHostView(WidgetItem item)219 private void applyPreviewOnAppWidgetHostView(WidgetItem item) { 220 if (mRemoteViewsPreview != null) { 221 mAppWidgetHostViewPreview = createAppWidgetHostView(getContext()); 222 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, 223 mRemoteViewsPreview); 224 return; 225 } 226 227 if (!item.hasPreviewLayout()) return; 228 229 Context context = getContext(); 230 // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview as 231 // a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, which 232 // supports applying local color extraction during drag & drop. 233 mAppWidgetHostViewPreview = isLauncherContext(context) 234 ? new LauncherAppWidgetHostView(context) 235 : createAppWidgetHostView(context); 236 LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo = 237 LauncherAppWidgetProviderInfo.fromProviderInfo(context, item.widgetInfo.clone()); 238 // A hack to force the initial layout to be the preview layout since there is no API for 239 // rendering a preview layout for work profile apps yet. For non-work profile layout, a 240 // proper solution is to use RemoteViews(PackageName, LayoutId). 241 launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout; 242 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, 243 launcherAppWidgetProviderInfo, /* remoteViews= */ null); 244 } 245 setAppWidgetHostViewPreview( NavigableAppWidgetHostView appWidgetHostViewPreview, LauncherAppWidgetProviderInfo providerInfo, @Nullable RemoteViews remoteViews)246 private void setAppWidgetHostViewPreview( 247 NavigableAppWidgetHostView appWidgetHostViewPreview, 248 LauncherAppWidgetProviderInfo providerInfo, 249 @Nullable RemoteViews remoteViews) { 250 appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 251 appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo); 252 Rect padding; 253 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 254 if (deviceProfile.shouldInsetWidgets()) { 255 padding = new Rect(); 256 appWidgetHostViewPreview.getWidgetInset(deviceProfile, padding); 257 } else { 258 padding = deviceProfile.inv.defaultWidgetPadding; 259 } 260 appWidgetHostViewPreview.setPadding(padding.left, padding.top, padding.right, 261 padding.bottom); 262 appWidgetHostViewPreview.updateAppWidget(remoteViews); 263 } 264 getWidgetView()265 public WidgetImageView getWidgetView() { 266 return mWidgetImage; 267 } 268 269 @Nullable getAppWidgetHostViewPreview()270 public NavigableAppWidgetHostView getAppWidgetHostViewPreview() { 271 return mAppWidgetHostViewPreview; 272 } 273 274 /** 275 * Sets if applying bitmap preview should be deferred. The UI will still load the bitmap, but 276 * will not cause invalidate, so that when deferring is disabled later, all the bitmaps are 277 * ready. 278 * This prevents invalidates while the animation is running. 279 */ setApplyBitmapDeferred(boolean isDeferred)280 public void setApplyBitmapDeferred(boolean isDeferred) { 281 if (mApplyBitmapDeferred != isDeferred) { 282 mApplyBitmapDeferred = isDeferred; 283 if (!mApplyBitmapDeferred && mDeferredDrawable != null) { 284 applyPreview(mDeferredDrawable); 285 mDeferredDrawable = null; 286 } 287 } 288 } 289 setAnimatePreview(boolean shouldAnimate)290 public void setAnimatePreview(boolean shouldAnimate) { 291 mAnimatePreview = shouldAnimate; 292 } 293 applyPreview(Bitmap bitmap)294 public void applyPreview(Bitmap bitmap) { 295 FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap); 296 applyPreview(new RoundDrawableWrapper(drawable, mEnforcedCornerRadius)); 297 } 298 applyPreview(Drawable drawable)299 private void applyPreview(Drawable drawable) { 300 if (mApplyBitmapDeferred) { 301 mDeferredDrawable = drawable; 302 return; 303 } 304 if (drawable != null) { 305 float scale = 1f; 306 if (getWidth() > 0 && getHeight() > 0) { 307 // Scale down the preview size if it's wider than the cell. 308 float maxWidth = getWidth(); 309 float previewWidth = drawable.getIntrinsicWidth() * mPreviewScale; 310 scale = Math.min(maxWidth / previewWidth, 1); 311 } 312 setContainerSize( 313 Math.round(drawable.getIntrinsicWidth() * scale), 314 Math.round(drawable.getIntrinsicHeight() * scale)); 315 mWidgetImage.setDrawable(drawable); 316 mWidgetImage.setVisibility(View.VISIBLE); 317 if (mAppWidgetHostViewPreview != null) { 318 removeView(mAppWidgetHostViewPreview); 319 mAppWidgetHostViewPreview = null; 320 } 321 } 322 if (mAnimatePreview) { 323 mWidgetImageContainer.setAlpha(0f); 324 ViewPropertyAnimator anim = mWidgetImageContainer.animate(); 325 anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); 326 } else { 327 mWidgetImageContainer.setAlpha(1f); 328 } 329 } 330 setContainerSize(int width, int height)331 private void setContainerSize(int width, int height) { 332 LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams(); 333 layoutParams.width = (int) (width * mPreviewScale); 334 layoutParams.height = (int) (height * mPreviewScale); 335 mWidgetImageContainer.setLayoutParams(layoutParams); 336 } 337 ensurePreview()338 public void ensurePreview() { 339 if (mAppWidgetHostViewPreview != null) { 340 setContainerSize(mPreviewWidth, mPreviewHeight); 341 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 342 mPreviewWidth, mPreviewHeight, Gravity.FILL); 343 mAppWidgetHostViewPreview.setLayoutParams(params); 344 mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0); 345 mWidgetImage.setVisibility(View.GONE); 346 applyPreview((Drawable) null); 347 return; 348 } 349 if (mActiveRequest != null) { 350 return; 351 } 352 mActiveRequest = mWidgetPreviewLoader.loadPreview( 353 BaseActivity.fromContext(getContext()), mItem, 354 new Size(mPreviewWidth, mPreviewHeight), 355 this::applyPreview); 356 } 357 358 /** Sets the widget preview image size in number of cells. */ setPreviewSize(WidgetItem widgetItem)359 public Size setPreviewSize(WidgetItem widgetItem) { 360 return setPreviewSize(widgetItem, 1f); 361 } 362 363 /** Sets the widget preview image size, in number of cells, and preview scale. */ setPreviewSize(WidgetItem widgetItem, float previewScale)364 public Size setPreviewSize(WidgetItem widgetItem, float previewScale) { 365 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 366 Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, widgetItem); 367 mPreviewWidth = widgetSize.getWidth(); 368 mPreviewHeight = widgetSize.getHeight(); 369 mPreviewScale = previewScale; 370 return widgetSize; 371 } 372 373 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)374 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 375 int oldTop, int oldRight, int oldBottom) { 376 removeOnLayoutChangeListener(this); 377 ensurePreview(); 378 } 379 380 @Override onTouchEvent(MotionEvent ev)381 public boolean onTouchEvent(MotionEvent ev) { 382 super.onTouchEvent(ev); 383 mLongPressHelper.onTouchEvent(ev); 384 return true; 385 } 386 387 @Override cancelLongPress()388 public void cancelLongPress() { 389 super.cancelLongPress(); 390 mLongPressHelper.cancelLongPress(); 391 } 392 393 /** 394 * Helper method to get the string info of the tag. 395 */ getTagToString()396 private String getTagToString() { 397 if (getTag() instanceof PendingAddWidgetInfo || 398 getTag() instanceof PendingAddShortcutInfo) { 399 return getTag().toString(); 400 } 401 return ""; 402 } 403 createAppWidgetHostView(Context context)404 private static NavigableAppWidgetHostView createAppWidgetHostView(Context context) { 405 return new NavigableAppWidgetHostView(context) { 406 @Override 407 protected boolean shouldAllowDirectClick() { 408 return false; 409 } 410 }; 411 } 412 413 private static boolean isLauncherContext(Context context) { 414 try { 415 Launcher.getLauncher(context); 416 return true; 417 } catch (Exception e) { 418 return false; 419 } 420 } 421 422 @Override 423 public CharSequence getAccessibilityClassName() { 424 return WidgetCell.class.getName(); 425 } 426 427 @Override 428 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 429 super.onInitializeAccessibilityNodeInfo(info); 430 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); 431 } 432 } 433