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