• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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