1 /*
2  * Copyright (C) 2014 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.graphics.Paint.ANTI_ALIAS_FLAG;
20 import static android.graphics.Paint.DITHER_FLAG;
21 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
22 
23 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
24 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
26 
27 import android.appwidget.AppWidgetProviderInfo;
28 import android.content.Context;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Matrix;
33 import android.graphics.Paint;
34 import android.graphics.PorterDuff;
35 import android.graphics.Rect;
36 import android.graphics.RectF;
37 import android.graphics.drawable.ColorDrawable;
38 import android.graphics.drawable.Drawable;
39 import android.os.Bundle;
40 import android.text.Layout;
41 import android.text.StaticLayout;
42 import android.text.TextPaint;
43 import android.text.TextUtils;
44 import android.util.SizeF;
45 import android.util.TypedValue;
46 import android.view.ContextThemeWrapper;
47 import android.view.View;
48 import android.view.View.OnClickListener;
49 import android.widget.RemoteViews;
50 
51 import androidx.annotation.NonNull;
52 import androidx.annotation.Nullable;
53 
54 import com.android.launcher3.DeviceProfile;
55 import com.android.launcher3.Launcher;
56 import com.android.launcher3.LauncherAppState;
57 import com.android.launcher3.R;
58 import com.android.launcher3.icons.FastBitmapDrawable;
59 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
60 import com.android.launcher3.model.data.ItemInfoWithIcon;
61 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
62 import com.android.launcher3.model.data.PackageItemInfo;
63 import com.android.launcher3.util.SafeCloseable;
64 import com.android.launcher3.util.Themes;
65 
66 import java.util.List;
67 
68 public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
69         implements OnClickListener, ItemInfoUpdateReceiver {
70     private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
71     private static final float MIN_SATURATION = 0.7f;
72 
73     private static final int FLAG_DRAW_SETTINGS = 1;
74     private static final int FLAG_DRAW_ICON = 2;
75     private static final int FLAG_DRAW_LABEL = 4;
76 
77     private static final int DEFERRED_ALPHA = 0x77;
78 
79     private final Rect mRect = new Rect();
80 
81     private final Matrix mMatrix = new Matrix();
82     private final RectF mPreviewBitmapRect = new RectF();
83     private final RectF mCanvasRect = new RectF();
84 
85     private final LauncherWidgetHolder mWidgetHolder;
86     private final LauncherAppWidgetProviderInfo mAppwidget;
87     private final LauncherAppWidgetInfo mInfo;
88     private final int mStartState;
89     private final boolean mDisabledForSafeMode;
90     private final CharSequence mLabel;
91 
92     private OnClickListener mClickListener;
93     private SafeCloseable mOnDetachCleanup;
94 
95     private int mDragFlags;
96 
97     private Drawable mCenterDrawable;
98     private Drawable mSettingIconDrawable;
99 
100     private boolean mDrawableSizeChanged;
101     private boolean mIsDeferredWidget;
102 
103     private final TextPaint mPaint;
104 
105     private final Paint mPreviewPaint;
106     private Layout mSetupTextLayout;
107 
108     @Nullable private Bitmap mPreviewBitmap;
109 
PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget)110     public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
111             LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) {
112         this(context, widgetHolder, info, appWidget, null);
113     }
114 
PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget, @Nullable Bitmap previewBitmap)115     public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
116             LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget,
117             @Nullable Bitmap previewBitmap) {
118         this(context, widgetHolder, info, appWidget,
119                 context.getResources().getText(R.string.gadget_complete_setup_text), previewBitmap);
120         super.updateAppWidget(null);
121         setOnClickListener(mActivityContext.getItemOnClickListener());
122 
123         if (info.pendingItemInfo == null) {
124             info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(),
125                     info.user);
126             LauncherAppState.getInstance(context).getIconCache()
127                     .updateIconInBackground(this, info.pendingItemInfo);
128         } else {
129             reapplyItemInfo(info.pendingItemInfo);
130         }
131     }
132 
PendingAppWidgetHostView( Context context, LauncherWidgetHolder widgetHolder, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget)133     public PendingAppWidgetHostView(
134             Context context, LauncherWidgetHolder widgetHolder,
135             int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
136         this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
137                 appWidget, appWidget.label, null);
138         getBackground().mutate().setAlpha(DEFERRED_ALPHA);
139 
140         mCenterDrawable = new ColorDrawable(Color.TRANSPARENT);
141         mDragFlags = FLAG_DRAW_LABEL;
142         mDrawableSizeChanged = true;
143         mIsDeferredWidget = true;
144     }
145 
146     /**
147      * Set {@link Bitmap} of widget preview and update background drawable. When showing preview
148      * bitmap, we shouldn't draw background.
149      */
setPreviewBitmapAndUpdateBackground(@ullable Bitmap previewBitmap)150     public void setPreviewBitmapAndUpdateBackground(@Nullable Bitmap previewBitmap) {
151         setBackgroundResource(previewBitmap != null ? 0 : R.drawable.pending_widget_bg);
152         if (this.mPreviewBitmap == previewBitmap) {
153             return;
154         }
155         this.mPreviewBitmap = previewBitmap;
156         invalidate();
157     }
158 
PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo appwidget, CharSequence label, @Nullable Bitmap previewBitmap)159     private PendingAppWidgetHostView(Context context,
160             LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info,
161             LauncherAppWidgetProviderInfo appwidget, CharSequence label,
162             @Nullable Bitmap previewBitmap) {
163         super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
164         mWidgetHolder = widgetHolder;
165         mAppwidget = appwidget;
166         mInfo = info;
167         mStartState = info.restoreStatus;
168         mDisabledForSafeMode = LauncherAppState.getInstance(context).isSafeModeEnabled();
169         mLabel = label;
170 
171         mPaint = new TextPaint();
172         mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
173         mPaint.setTextSize(TypedValue.applyDimension(
174                 TypedValue.COMPLEX_UNIT_PX,
175                 mActivityContext.getDeviceProfile().iconTextSizePx,
176                 getResources().getDisplayMetrics()));
177         mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
178 
179         setWillNotDraw(false);
180         setPreviewBitmapAndUpdateBackground(previewBitmap);
181     }
182 
183     @Override
getAppWidgetInfo()184     public AppWidgetProviderInfo getAppWidgetInfo() {
185         return mAppwidget;
186     }
187 
188     @Override
getAppWidgetId()189     public int getAppWidgetId() {
190         return mInfo.appWidgetId;
191     }
192 
193     @Override
updateAppWidget(RemoteViews remoteViews)194     public void updateAppWidget(RemoteViews remoteViews) {
195         checkIfRestored();
196     }
197 
checkIfRestored()198     private void checkIfRestored() {
199         WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext());
200         if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) {
201             MAIN_EXECUTOR.getHandler().post(this::reInflate);
202         }
203     }
204 
isDeferredWidget()205     public boolean isDeferredWidget() {
206         return mIsDeferredWidget;
207     }
208 
209     @Override
onAttachedToWindow()210     protected void onAttachedToWindow() {
211         super.onAttachedToWindow();
212 
213         if ((mAppwidget != null)
214                 && !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)
215                 && mInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) {
216             // If the widget is not completely restored, but has a valid ID, then listen of
217             // updates from provider app for potential restore complete.
218             if (mOnDetachCleanup != null) {
219                 mOnDetachCleanup.close();
220             }
221             mOnDetachCleanup = mWidgetHolder.addOnUpdateListener(
222                     mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
223             checkIfRestored();
224         }
225     }
226 
227     @Override
onDetachedFromWindow()228     protected void onDetachedFromWindow() {
229         super.onDetachedFromWindow();
230         if (mOnDetachCleanup != null) {
231             mOnDetachCleanup.close();
232             mOnDetachCleanup = null;
233         }
234     }
235 
236     /**
237      * Forces the Launcher to reinflate the widget view
238      */
reInflate()239     public void reInflate() {
240         if (!isAttachedToWindow()) {
241             return;
242         }
243         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
244         if (info == null) {
245             // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
246             return;
247         }
248         if (mActivityContext instanceof Launcher launcher) {
249             // Remove and rebind the current widget (which was inflated in the wrong
250             // orientation), but don't delete it from the database
251             launcher.removeItem(this, info, false  /* deleteFromDb */,
252                     "widget removed because of configuration change");
253             launcher.bindAppWidget(info);
254         }
255     }
256 
257     @Override
updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, int maxHeight)258     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
259             int maxHeight) {
260         // No-op
261     }
262 
263     @Override
updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes)264     public void updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes) {
265         // No-op
266     }
267 
268     @Override
getDefaultView()269     protected View getDefaultView() {
270         View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
271         defaultView.setOnClickListener(this);
272         applyState();
273         invalidate();
274         return defaultView;
275     }
276 
277     @Override
setOnClickListener(OnClickListener l)278     public void setOnClickListener(OnClickListener l) {
279         mClickListener = l;
280     }
281 
isReinflateIfNeeded()282     public boolean isReinflateIfNeeded() {
283         return mStartState != mInfo.restoreStatus;
284     }
285 
286     @Override
onSizeChanged(int w, int h, int oldw, int oldh)287     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
288         super.onSizeChanged(w, h, oldw, oldh);
289         mDrawableSizeChanged = true;
290     }
291 
292     @Override
reapplyItemInfo(ItemInfoWithIcon info)293     public void reapplyItemInfo(ItemInfoWithIcon info) {
294         if (mCenterDrawable != null) {
295             mCenterDrawable.setCallback(null);
296             mCenterDrawable = null;
297         }
298         mDragFlags = 0;
299         if (info.bitmap.icon != null) {
300             mDragFlags = FLAG_DRAW_ICON;
301 
302             Drawable widgetCategoryIcon = getWidgetCategoryIcon();
303             // The view displays three modes,
304             //   1) App icon in the center
305             //   2) Preload icon in the center
306             //   3) App icon in the center with a setup icon on the top left corner.
307             if (mDisabledForSafeMode) {
308                 if (widgetCategoryIcon == null) {
309                     FastBitmapDrawable disabledIcon = info.newIcon(getContext());
310                     disabledIcon.setIsDisabled(true);
311                     mCenterDrawable = disabledIcon;
312                 } else {
313                     widgetCategoryIcon.setColorFilter(getDisabledColorFilter());
314                     mCenterDrawable = widgetCategoryIcon;
315                 }
316                 mSettingIconDrawable = null;
317             } else if (isReadyForClickSetup()) {
318                 mCenterDrawable = widgetCategoryIcon == null
319                         ? info.newIcon(getContext())
320                         : widgetCategoryIcon;
321                 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
322                 updateSettingColor(info.bitmap.color);
323 
324                 mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL;
325             } else {
326                 mCenterDrawable = widgetCategoryIcon == null
327                         ? newPendingIcon(getContext(), info)
328                         : widgetCategoryIcon;
329                 mSettingIconDrawable = null;
330                 applyState();
331             }
332             mCenterDrawable.setCallback(this);
333             mDrawableSizeChanged = true;
334         }
335         invalidate();
336     }
337 
updateSettingColor(int dominantColor)338     private void updateSettingColor(int dominantColor) {
339         // Make the dominant color bright.
340         float[] hsv = new float[3];
341         Color.colorToHSV(dominantColor, hsv);
342         hsv[1] = Math.min(hsv[1], MIN_SATURATION);
343         hsv[2] = 1;
344         mSettingIconDrawable.setColorFilter(Color.HSVToColor(hsv),  PorterDuff.Mode.SRC_IN);
345     }
346 
347     @Override
verifyDrawable(Drawable who)348     protected boolean verifyDrawable(Drawable who) {
349         return (who == mCenterDrawable) || super.verifyDrawable(who);
350     }
351 
applyState()352     public void applyState() {
353         if (mCenterDrawable != null) {
354             mCenterDrawable.setLevel(Math.max(mInfo.installProgress, 0));
355         }
356     }
357 
358     @Override
onClick(View v)359     public void onClick(View v) {
360         // AppWidgetHostView blocks all click events on the root view. Instead handle click events
361         // on the content and pass it along.
362         if (mClickListener != null) {
363             mClickListener.onClick(this);
364         }
365     }
366 
367     /**
368      * A pending widget is ready for setup after the provider is installed and
369      *   1) Widget id is not valid: the widget id is not yet bound to the provider, probably
370      *                              because the launcher doesn't have appropriate permissions.
371      *                              Note that we would still have an allocated id as that does not
372      *                              require any permissions and can be done during view inflation.
373      *   2) UI is not ready: the id is valid and the bound. But the widget has a configure activity
374      *                       which needs to be called once.
375      */
isReadyForClickSetup()376     public boolean isReadyForClickSetup() {
377         return !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
378                 && (mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
379                 || mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID));
380     }
381 
updateDrawableBounds()382     private void updateDrawableBounds() {
383         DeviceProfile grid = mActivityContext.getDeviceProfile();
384         int paddingTop = getPaddingTop();
385         int paddingBottom = getPaddingBottom();
386         int paddingLeft = getPaddingLeft();
387         int paddingRight = getPaddingRight();
388 
389         int minPadding = getResources()
390                 .getDimensionPixelSize(R.dimen.pending_widget_min_padding);
391 
392         int availableWidth = getWidth() - paddingLeft - paddingRight - 2 * minPadding;
393         int availableHeight = getHeight() - paddingTop - paddingBottom - 2 * minPadding;
394 
395         float iconSize = ((mDragFlags & FLAG_DRAW_ICON) == 0) ? 0
396                 : Math.max(0, Math.min(availableWidth, availableHeight));
397         // Use twice the setting size factor, as the setting is drawn at a corner and the
398         // icon is drawn in the center.
399         float settingIconScaleFactor = ((mDragFlags & FLAG_DRAW_SETTINGS) == 0) ? 0
400                 : 1 + SETUP_ICON_SIZE_FACTOR * 2;
401 
402         int maxSize = Math.max(availableWidth, availableHeight);
403         if (iconSize * settingIconScaleFactor > maxSize) {
404             // There is an overlap
405             iconSize = maxSize / settingIconScaleFactor;
406         }
407 
408         int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx);
409 
410         // Icon top when we do not draw the text
411         int iconTop = (getHeight() - actualIconSize) / 2;
412         mSetupTextLayout = null;
413 
414         if (availableWidth > 0 && !TextUtils.isEmpty(mLabel)
415                 && ((mDragFlags & FLAG_DRAW_LABEL) != 0)) {
416             // Recreate the setup text.
417             mSetupTextLayout = new StaticLayout(
418                     mLabel, mPaint, availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true);
419             int textHeight = mSetupTextLayout.getHeight();
420 
421             // Extra icon size due to the setting icon
422             float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor
423                     + grid.iconDrawablePaddingPx;
424 
425             if (minHeightWithText < availableHeight) {
426                 // We can draw the text as well
427                 iconTop = (getHeight() - textHeight
428                         - grid.iconDrawablePaddingPx - actualIconSize) / 2;
429 
430             } else {
431                 // We can't draw the text. Let the iconTop be same as before.
432                 mSetupTextLayout = null;
433             }
434         }
435 
436         mRect.set(0, 0, actualIconSize, actualIconSize);
437         mRect.offset((getWidth() - actualIconSize) / 2, iconTop);
438         mCenterDrawable.setBounds(mRect);
439 
440         if (mSettingIconDrawable != null) {
441             mRect.left = paddingLeft + minPadding;
442             mRect.right = mRect.left + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize);
443             mRect.top = paddingTop + minPadding;
444             mRect.bottom = mRect.top + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize);
445             mSettingIconDrawable.setBounds(mRect);
446         }
447 
448         if (mSetupTextLayout != null) {
449             // Set up position for dragging the text
450             mRect.left = paddingLeft + minPadding;
451             mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx;
452         }
453     }
454 
455     @Override
onDraw(Canvas canvas)456     protected void onDraw(Canvas canvas) {
457         if (mPreviewBitmap != null
458                 && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) {
459             mPreviewBitmapRect.set(0, 0, mPreviewBitmap.getWidth(), mPreviewBitmap.getHeight());
460             mCanvasRect.set(0, 0, getWidth(), getHeight());
461 
462             mMatrix.setRectToRect(mPreviewBitmapRect, mCanvasRect, Matrix.ScaleToFit.CENTER);
463             canvas.drawBitmap(mPreviewBitmap, mMatrix, mPreviewPaint);
464             return;
465         }
466         if (mCenterDrawable == null) {
467             // Nothing to draw
468             return;
469         }
470 
471         if (mDrawableSizeChanged) {
472             updateDrawableBounds();
473             mDrawableSizeChanged = false;
474         }
475 
476         mCenterDrawable.draw(canvas);
477         if (mSettingIconDrawable != null) {
478             mSettingIconDrawable.draw(canvas);
479         }
480         if (mSetupTextLayout != null) {
481             canvas.save();
482             canvas.translate(mRect.left, mRect.top);
483             mSetupTextLayout.draw(canvas);
484             canvas.restore();
485         }
486     }
487 
488     /**
489      * Returns the widget category icon for {@link #mInfo}.
490      *
491      * <p>If {@link #mInfo}'s category is {@code PackageItemInfo#NO_CATEGORY} or unknown, returns
492      * {@code null}.
493      */
494     @Nullable
getWidgetCategoryIcon()495     private Drawable getWidgetCategoryIcon() {
496         if (mInfo.pendingItemInfo.widgetCategory == WidgetSections.NO_CATEGORY) {
497             return null;
498         }
499         return mInfo.pendingItemInfo.newIcon(getContext());
500     }
501 }
502