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.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; 21 import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx; 22 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.drawable.Drawable; 26 import android.os.Process; 27 import android.text.TextUtils; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.util.Size; 31 import android.view.Gravity; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.ViewPropertyAnimator; 36 import android.view.accessibility.AccessibilityNodeInfo; 37 import android.widget.FrameLayout; 38 import android.widget.ImageView; 39 import android.widget.LinearLayout; 40 import android.widget.RemoteViews; 41 import android.widget.TextView; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 46 import com.android.launcher3.CheckLongPressHelper; 47 import com.android.launcher3.Launcher; 48 import com.android.launcher3.R; 49 import com.android.launcher3.icons.FastBitmapDrawable; 50 import com.android.launcher3.icons.RoundDrawableWrapper; 51 import com.android.launcher3.icons.cache.HandlerRunnable; 52 import com.android.launcher3.model.WidgetItem; 53 import com.android.launcher3.views.ActivityContext; 54 55 import java.util.function.Consumer; 56 57 /** 58 * Represents the individual cell of the widget inside the widget tray. The preview is drawn 59 * horizontally centered, and scaled down if needed. 60 * 61 * This view does not support padding. Since the image is scaled down to fit the view, padding will 62 * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth 63 * transition from the view to drag view, so when adding padding support, DnD would need to 64 * consider the appropriate scaling factor. 65 */ 66 public class WidgetCell extends LinearLayout { 67 68 private static final String TAG = "WidgetCell"; 69 private static final boolean DEBUG = false; 70 71 private static final int FADE_IN_DURATION_MS = 90; 72 73 /** 74 * The requested scale of the preview container. It can be lower than this as well. 75 */ 76 private float mPreviewContainerScale = 1f; 77 78 private FrameLayout mWidgetImageContainer; 79 private WidgetImageView mWidgetImage; 80 private ImageView mWidgetBadge; 81 private TextView mWidgetName; 82 private TextView mWidgetDims; 83 private TextView mWidgetDescription; 84 85 private WidgetItem mItem; 86 private Size mWidgetSize; 87 88 private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader; 89 90 protected HandlerRunnable mActiveRequest; 91 private boolean mAnimatePreview = true; 92 93 protected final ActivityContext mActivity; 94 private final CheckLongPressHelper mLongPressHelper; 95 private final float mEnforcedCornerRadius; 96 97 private RemoteViews mRemoteViewsPreview; 98 private NavigableAppWidgetHostView mAppWidgetHostViewPreview; 99 private float mAppWidgetHostViewScale = 1f; 100 private int mSourceContainer = CONTAINER_WIDGETS_TRAY; 101 WidgetCell(Context context)102 public WidgetCell(Context context) { 103 this(context, null); 104 } 105 WidgetCell(Context context, AttributeSet attrs)106 public WidgetCell(Context context, AttributeSet attrs) { 107 this(context, attrs, 0); 108 } 109 WidgetCell(Context context, AttributeSet attrs, int defStyle)110 public WidgetCell(Context context, AttributeSet attrs, int defStyle) { 111 super(context, attrs, defStyle); 112 113 mActivity = ActivityContext.lookupContext(context); 114 mWidgetPreviewLoader = new DatabaseWidgetPreviewLoader(context); 115 mLongPressHelper = new CheckLongPressHelper(this); 116 mLongPressHelper.setLongPressTimeoutFactor(1); 117 mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context); 118 mWidgetSize = new Size(0, 0); 119 120 setClipToPadding(false); 121 setAccessibilityDelegate(mActivity.getAccessibilityDelegate()); 122 } 123 124 @Override onFinishInflate()125 protected void onFinishInflate() { 126 super.onFinishInflate(); 127 128 mWidgetImageContainer = findViewById(R.id.widget_preview_container); 129 mWidgetImage = findViewById(R.id.widget_preview); 130 mWidgetBadge = findViewById(R.id.widget_badge); 131 mWidgetName = findViewById(R.id.widget_name); 132 mWidgetDims = findViewById(R.id.widget_dims); 133 mWidgetDescription = findViewById(R.id.widget_description); 134 } 135 setRemoteViewsPreview(RemoteViews view)136 public void setRemoteViewsPreview(RemoteViews view) { 137 mRemoteViewsPreview = view; 138 } 139 140 @Nullable getRemoteViewsPreview()141 public RemoteViews getRemoteViewsPreview() { 142 return mRemoteViewsPreview; 143 } 144 145 /** Returns the app widget host view scale, which is a value between [0f, 1f]. */ getAppWidgetHostViewScale()146 public float getAppWidgetHostViewScale() { 147 return mAppWidgetHostViewScale; 148 } 149 150 /** 151 * Called to clear the view and free attached resources. (e.g., {@link Bitmap} 152 */ clear()153 public void clear() { 154 if (DEBUG) { 155 Log.d(TAG, "reset called on:" + mWidgetName.getText()); 156 } 157 mWidgetImage.animate().cancel(); 158 mWidgetImage.setDrawable(null); 159 mWidgetImage.setVisibility(View.VISIBLE); 160 mWidgetBadge.setImageDrawable(null); 161 mWidgetBadge.setVisibility(View.GONE); 162 mWidgetName.setText(null); 163 mWidgetDims.setText(null); 164 mWidgetDescription.setText(null); 165 mWidgetDescription.setVisibility(GONE); 166 167 if (mActiveRequest != null) { 168 mActiveRequest.cancel(); 169 mActiveRequest = null; 170 } 171 mRemoteViewsPreview = null; 172 if (mAppWidgetHostViewPreview != null) { 173 mWidgetImageContainer.removeView(mAppWidgetHostViewPreview); 174 } 175 mAppWidgetHostViewPreview = null; 176 mAppWidgetHostViewScale = 1f; 177 mPreviewContainerScale = 1f; 178 mItem = null; 179 mWidgetSize = new Size(0, 0); 180 } 181 setSourceContainer(int sourceContainer)182 public void setSourceContainer(int sourceContainer) { 183 this.mSourceContainer = sourceContainer; 184 } 185 186 /** 187 * Applies the item to this view 188 */ applyFromCellItem(WidgetItem item)189 public void applyFromCellItem(WidgetItem item) { 190 applyFromCellItem(item, 1f); 191 } 192 193 /** 194 * Applies the item to this view 195 */ applyFromCellItem(WidgetItem item, float previewScale)196 public void applyFromCellItem(WidgetItem item, float previewScale) { 197 applyFromCellItem(item, previewScale, this::applyPreview, null); 198 } 199 200 /** 201 * Applies the item to this view 202 * @param item item to apply 203 * @param previewScale factor to scale the preview 204 * @param callback callback when preview is loaded in case the preview is being loaded or cached 205 * @param cachedPreview previously cached preview bitmap is present 206 */ applyFromCellItem(WidgetItem item, float previewScale, @NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview)207 public void applyFromCellItem(WidgetItem item, float previewScale, 208 @NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) { 209 mPreviewContainerScale = previewScale; 210 211 Context context = getContext(); 212 mItem = item; 213 mWidgetSize = getWidgetItemSizePx(getContext(), mActivity.getDeviceProfile(), mItem); 214 215 mWidgetName.setText(mItem.label); 216 mWidgetName.setContentDescription( 217 context.getString(R.string.widget_preview_context_description, mItem.label)); 218 mWidgetDims.setText(context.getString(R.string.widget_dims_format, 219 mItem.spanX, mItem.spanY)); 220 mWidgetDims.setContentDescription(context.getString( 221 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY)); 222 if (!TextUtils.isEmpty(mItem.description)) { 223 mWidgetDescription.setText(mItem.description); 224 mWidgetDescription.setVisibility(VISIBLE); 225 } else { 226 mWidgetDescription.setVisibility(GONE); 227 } 228 229 if (item.activityInfo != null) { 230 setTag(new PendingAddShortcutInfo(item.activityInfo)); 231 } else { 232 setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer)); 233 } 234 235 if (mRemoteViewsPreview != null) { 236 mAppWidgetHostViewPreview = createAppWidgetHostView(context); 237 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo, 238 mRemoteViewsPreview); 239 } else if (item.hasPreviewLayout()) { 240 // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview 241 // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, 242 // which supports applying local color extraction during drag & drop. 243 mAppWidgetHostViewPreview = isLauncherContext(context) 244 ? new LauncherAppWidgetHostView(context) 245 : createAppWidgetHostView(context); 246 LauncherAppWidgetProviderInfo providerInfo = 247 fromProviderInfo(context, item.widgetInfo.clone()); 248 // A hack to force the initial layout to be the preview layout since there is no API for 249 // rendering a preview layout for work profile apps yet. For non-work profile layout, a 250 // proper solution is to use RemoteViews(PackageName, LayoutId). 251 providerInfo.initialLayout = item.widgetInfo.previewLayout; 252 setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, providerInfo, null); 253 } else if (cachedPreview != null) { 254 applyPreview(cachedPreview); 255 } else { 256 if (mActiveRequest == null) { 257 mActiveRequest = mWidgetPreviewLoader.loadPreview(mItem, mWidgetSize, callback); 258 } 259 } 260 } 261 setAppWidgetHostViewPreview( NavigableAppWidgetHostView appWidgetHostViewPreview, LauncherAppWidgetProviderInfo providerInfo, @Nullable RemoteViews remoteViews)262 private void setAppWidgetHostViewPreview( 263 NavigableAppWidgetHostView appWidgetHostViewPreview, 264 LauncherAppWidgetProviderInfo providerInfo, 265 @Nullable RemoteViews remoteViews) { 266 appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 267 appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo); 268 appWidgetHostViewPreview.updateAppWidget(remoteViews); 269 appWidgetHostViewPreview.setClipToPadding(false); 270 appWidgetHostViewPreview.setClipChildren(false); 271 272 FrameLayout.LayoutParams widgetHostLP = new FrameLayout.LayoutParams( 273 mWidgetSize.getWidth(), mWidgetSize.getHeight(), Gravity.CENTER); 274 mWidgetImageContainer.addView(appWidgetHostViewPreview, /* index= */ 0, widgetHostLP); 275 mWidgetImage.setVisibility(View.GONE); 276 applyPreview(null); 277 278 appWidgetHostViewPreview.addOnLayoutChangeListener( 279 (v, l, t, r, b, ol, ot, or, ob) -> 280 updateAppWidgetHostScale(appWidgetHostViewPreview)); 281 } 282 updateAppWidgetHostScale(NavigableAppWidgetHostView view)283 private void updateAppWidgetHostScale(NavigableAppWidgetHostView view) { 284 // Scale the content such that all of the content is visible 285 int contentWidth = view.getWidth(); 286 int contentHeight = view.getHeight(); 287 288 if (view.getChildCount() == 1) { 289 View content = view.getChildAt(0); 290 // Take the content width based on the edge furthest from the center, so that when 291 // scaling the hostView, the farthest edge is still visible. 292 contentWidth = 2 * Math.max(contentWidth / 2 - content.getLeft(), 293 content.getRight() - contentWidth / 2); 294 contentHeight = 2 * Math.max(contentHeight / 2 - content.getTop(), 295 content.getBottom() - contentHeight / 2); 296 } 297 298 if (contentWidth <= 0 || contentHeight <= 0) { 299 mAppWidgetHostViewScale = 1; 300 } else { 301 float pWidth = mWidgetImageContainer.getWidth(); 302 float pHeight = mWidgetImageContainer.getHeight(); 303 mAppWidgetHostViewScale = Math.min(pWidth / contentWidth, pHeight / contentHeight); 304 } 305 view.setScaleToFit(mAppWidgetHostViewScale); 306 } 307 getWidgetView()308 public WidgetImageView getWidgetView() { 309 return mWidgetImage; 310 } 311 312 @Nullable getAppWidgetHostViewPreview()313 public NavigableAppWidgetHostView getAppWidgetHostViewPreview() { 314 return mAppWidgetHostViewPreview; 315 } 316 setAnimatePreview(boolean shouldAnimate)317 public void setAnimatePreview(boolean shouldAnimate) { 318 mAnimatePreview = shouldAnimate; 319 } 320 applyPreview(Bitmap bitmap)321 private void applyPreview(Bitmap bitmap) { 322 if (bitmap != null) { 323 Drawable drawable = new RoundDrawableWrapper( 324 new FastBitmapDrawable(bitmap), mEnforcedCornerRadius); 325 mWidgetImage.setDrawable(drawable); 326 mWidgetImage.setVisibility(View.VISIBLE); 327 if (mAppWidgetHostViewPreview != null) { 328 removeView(mAppWidgetHostViewPreview); 329 mAppWidgetHostViewPreview = null; 330 } 331 } 332 333 if (mAnimatePreview) { 334 mWidgetImageContainer.setAlpha(0f); 335 ViewPropertyAnimator anim = mWidgetImageContainer.animate(); 336 anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); 337 } else { 338 mWidgetImageContainer.setAlpha(1f); 339 } 340 if (mActiveRequest != null) { 341 mActiveRequest.cancel(); 342 mActiveRequest = null; 343 } 344 } 345 346 /** Used to show the badge when the widget is in the recommended section 347 */ showBadge()348 public void showBadge() { 349 if (Process.myUserHandle().equals(mItem.user)) { 350 mWidgetBadge.setVisibility(View.GONE); 351 } else { 352 mWidgetBadge.setVisibility(View.VISIBLE); 353 mWidgetBadge.setImageResource(R.drawable.ic_work_app_badge); 354 } 355 } 356 357 @Override onTouchEvent(MotionEvent ev)358 public boolean onTouchEvent(MotionEvent ev) { 359 super.onTouchEvent(ev); 360 mLongPressHelper.onTouchEvent(ev); 361 return true; 362 } 363 364 @Override cancelLongPress()365 public void cancelLongPress() { 366 super.cancelLongPress(); 367 mLongPressHelper.cancelLongPress(); 368 } 369 createAppWidgetHostView(Context context)370 private static NavigableAppWidgetHostView createAppWidgetHostView(Context context) { 371 return new NavigableAppWidgetHostView(context) { 372 @Override 373 protected boolean shouldAllowDirectClick() { 374 return false; 375 } 376 }; 377 } 378 379 private static boolean isLauncherContext(Context context) { 380 return ActivityContext.lookupContext(context) instanceof Launcher; 381 } 382 383 @Override 384 public CharSequence getAccessibilityClassName() { 385 return WidgetCell.class.getName(); 386 } 387 388 @Override 389 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 390 super.onInitializeAccessibilityNodeInfo(info); 391 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); 392 } 393 394 @Override 395 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 396 ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams(); 397 398 mAppWidgetHostViewScale = mPreviewContainerScale; 399 int maxWidth = MeasureSpec.getSize(widthMeasureSpec); 400 containerLp.width = Math.round(mWidgetSize.getWidth() * mAppWidgetHostViewScale); 401 if (containerLp.width > maxWidth) { 402 containerLp.width = maxWidth; 403 mAppWidgetHostViewScale = (float) containerLp.width / mWidgetSize.getWidth(); 404 } 405 containerLp.height = Math.round(mWidgetSize.getHeight() * mAppWidgetHostViewScale); 406 // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass 407 408 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 409 } 410 } 411