• 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.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