• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 android.appwidget.AppWidgetProviderInfo;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.graphics.Canvas;
23 import android.graphics.Outline;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.os.Handler;
27 import android.os.SystemClock;
28 import android.util.SparseBooleanArray;
29 import android.util.SparseIntArray;
30 import android.view.LayoutInflater;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.ViewDebug;
34 import android.view.ViewGroup;
35 import android.view.ViewOutlineProvider;
36 import android.view.accessibility.AccessibilityNodeInfo;
37 import android.widget.AdapterView;
38 import android.widget.Advanceable;
39 import android.widget.RemoteViews;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.UiThread;
44 
45 import com.android.launcher3.CheckLongPressHelper;
46 import com.android.launcher3.Launcher;
47 import com.android.launcher3.R;
48 import com.android.launcher3.Utilities;
49 import com.android.launcher3.Workspace;
50 import com.android.launcher3.dragndrop.DragLayer;
51 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
52 import com.android.launcher3.model.data.ItemInfo;
53 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
54 import com.android.launcher3.util.Executors;
55 import com.android.launcher3.util.Themes;
56 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
57 import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
58 
59 import java.util.List;
60 
61 /**
62  * {@inheritDoc}
63  */
64 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
65         implements TouchCompleteListener, View.OnLongClickListener,
66         LocalColorExtractor.Listener {
67 
68     private static final String LOG_TAG = "LauncherAppWidgetHostView";
69 
70     // Related to the auto-advancing of widgets
71     private static final long ADVANCE_INTERVAL = 20000;
72     private static final long ADVANCE_STAGGER = 250;
73 
74     // Maintains a list of widget ids which are supposed to be auto advanced.
75     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
76     // Maximum duration for which updates can be deferred.
77     private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
78 
79     protected final LayoutInflater mInflater;
80 
81     private final CheckLongPressHelper mLongPressHelper;
82     protected final Launcher mLauncher;
83     private final Workspace mWorkspace;
84 
85     @ViewDebug.ExportedProperty(category = "launcher")
86     private boolean mReinflateOnConfigChange;
87 
88     // Maintain the color manager.
89     private final LocalColorExtractor mColorExtractor;
90 
91     private boolean mIsScrollable;
92     private boolean mIsAttachedToWindow;
93     private boolean mIsAutoAdvanceRegistered;
94     private boolean mIsInDragMode = false;
95     private Runnable mAutoAdvanceRunnable;
96     private RectF mLastLocationRegistered = null;
97     @Nullable private AppWidgetHostViewDragListener mDragListener;
98 
99     // Used to store the widget sizes in drag layer coordinates.
100     private final Rect mCurrentWidgetSize = new Rect();
101     private final Rect mWidgetSizeAtDrag = new Rect();
102 
103     private final RectF mTempRectF = new RectF();
104     private final Rect mEnforcedRectangle = new Rect();
105     private final float mEnforcedCornerRadius;
106     private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
107         @Override
108         public void getOutline(View view, Outline outline) {
109             if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
110                 outline.setEmpty();
111             } else {
112                 outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
113             }
114         }
115     };
116     private final Object mUpdateLock = new Object();
117     private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
118     private long mDeferUpdatesUntilMillis = 0;
119     private RemoteViews mDeferredRemoteViews;
120     private boolean mHasDeferredColorChange = false;
121     private @Nullable SparseIntArray mDeferredColorChange = null;
122     private boolean mEnableColorExtraction = true;
123 
LauncherAppWidgetHostView(Context context)124     public LauncherAppWidgetHostView(Context context) {
125         super(context);
126         mLauncher = Launcher.getLauncher(context);
127         mWorkspace = mLauncher.getWorkspace();
128         mLongPressHelper = new CheckLongPressHelper(this, this);
129         mInflater = LayoutInflater.from(context);
130         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
131         setBackgroundResource(R.drawable.widget_internal_focus_bg);
132 
133         setExecutor(Executors.THREAD_POOL_EXECUTOR);
134         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
135             setOnLightBackground(true);
136         }
137         mColorExtractor = LocalColorExtractor.newInstance(getContext());
138         mColorExtractor.setListener(this);
139 
140         mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
141         mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
142     }
143 
144     @Override
setColorResources(@ullable SparseIntArray colors)145     public void setColorResources(@Nullable SparseIntArray colors) {
146         if (colors == null) {
147             resetColorResources();
148         } else {
149             super.setColorResources(colors);
150         }
151     }
152 
153     @Override
onDraw(Canvas canvas)154     protected void onDraw(Canvas canvas) {
155         super.onDraw(canvas);
156         if (mIsInDragMode && mDragListener != null) {
157             mDragListener.onDragContentChanged();
158         }
159     }
160 
161     @Override
onLongClick(View view)162     public boolean onLongClick(View view) {
163         if (mIsScrollable) {
164             DragLayer dragLayer = mLauncher.getDragLayer();
165             dragLayer.requestDisallowInterceptTouchEvent(false);
166         }
167         view.performLongClick();
168         return true;
169     }
170 
171     @Override
getErrorView()172     protected View getErrorView() {
173         return mInflater.inflate(R.layout.appwidget_error, this, false);
174     }
175 
176     @Override
updateAppWidget(RemoteViews remoteViews)177     public void updateAppWidget(RemoteViews remoteViews) {
178         synchronized (mUpdateLock) {
179             if (isDeferringUpdates()) {
180                 mDeferredRemoteViews = remoteViews;
181                 return;
182             }
183             mDeferredRemoteViews = null;
184         }
185 
186         super.updateAppWidget(remoteViews);
187 
188         // The provider info or the views might have changed.
189         checkIfAutoAdvance();
190 
191         // It is possible that widgets can receive updates while launcher is not in the foreground.
192         // Consequently, the widgets will be inflated for the orientation of the foreground activity
193         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
194         // orientation.
195         mReinflateOnConfigChange = !isSameOrientation();
196     }
197 
isSameOrientation()198     private boolean isSameOrientation() {
199         return mLauncher.getResources().getConfiguration().orientation ==
200                 mLauncher.getOrientation();
201     }
202 
checkScrollableRecursively(ViewGroup viewGroup)203     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
204         if (viewGroup instanceof AdapterView) {
205             return true;
206         } else {
207             for (int i = 0; i < viewGroup.getChildCount(); i++) {
208                 View child = viewGroup.getChildAt(i);
209                 if (child instanceof ViewGroup) {
210                     if (checkScrollableRecursively((ViewGroup) child)) {
211                         return true;
212                     }
213                 }
214             }
215         }
216         return false;
217     }
218 
219     /**
220      * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and
221      * colors through {@link #onColorsChanged} are currently being deferred.
222      * @see #beginDeferringUpdates()
223      */
isDeferringUpdates()224     private boolean isDeferringUpdates() {
225         return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis;
226     }
227 
228     /**
229      * Begin deferring the application of any {@link RemoteViews} updates made through
230      * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until
231      * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or
232      * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed.
233      */
beginDeferringUpdates()234     public void beginDeferringUpdates() {
235         synchronized (mUpdateLock) {
236             mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
237         }
238     }
239 
240     /**
241      * Stop deferring the application of {@link RemoteViews} updates made through
242      * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply
243      * any deferred updates.
244      */
endDeferringUpdates()245     public void endDeferringUpdates() {
246         RemoteViews remoteViews;
247         SparseIntArray deferredColors;
248         boolean hasDeferredColors;
249         synchronized (mUpdateLock) {
250             mDeferUpdatesUntilMillis = 0;
251             remoteViews = mDeferredRemoteViews;
252             mDeferredRemoteViews = null;
253             deferredColors = mDeferredColorChange;
254             hasDeferredColors = mHasDeferredColorChange;
255             mDeferredColorChange = null;
256             mHasDeferredColorChange = false;
257         }
258         if (remoteViews != null) {
259             updateAppWidget(remoteViews);
260         }
261         if (hasDeferredColors) {
262             onColorsChanged(null /* rectF */, deferredColors);
263         }
264     }
265 
onInterceptTouchEvent(MotionEvent ev)266     public boolean onInterceptTouchEvent(MotionEvent ev) {
267         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
268             DragLayer dragLayer = mLauncher.getDragLayer();
269             if (mIsScrollable) {
270                 dragLayer.requestDisallowInterceptTouchEvent(true);
271             }
272             dragLayer.setTouchCompleteListener(this);
273         }
274         mLongPressHelper.onTouchEvent(ev);
275         return mLongPressHelper.hasPerformedLongPress();
276     }
277 
onTouchEvent(MotionEvent ev)278     public boolean onTouchEvent(MotionEvent ev) {
279         mLongPressHelper.onTouchEvent(ev);
280         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
281         return true;
282     }
283 
284     @Override
onAttachedToWindow()285     protected void onAttachedToWindow() {
286         super.onAttachedToWindow();
287 
288         mIsAttachedToWindow = true;
289         checkIfAutoAdvance();
290 
291         if (mLastLocationRegistered != null) {
292             mColorExtractor.addLocation(List.of(mLastLocationRegistered));
293         }
294     }
295 
296     @Override
onDetachedFromWindow()297     protected void onDetachedFromWindow() {
298         super.onDetachedFromWindow();
299 
300         // We can't directly use isAttachedToWindow() here, as this is called before the internal
301         // state is updated. So isAttachedToWindow() will return true until next frame.
302         mIsAttachedToWindow = false;
303         checkIfAutoAdvance();
304         mColorExtractor.removeLocations();
305     }
306 
307     @Override
cancelLongPress()308     public void cancelLongPress() {
309         super.cancelLongPress();
310         mLongPressHelper.cancelLongPress();
311     }
312 
313     @Override
getAppWidgetInfo()314     public AppWidgetProviderInfo getAppWidgetInfo() {
315         AppWidgetProviderInfo info = super.getAppWidgetInfo();
316         if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
317             throw new IllegalStateException("Launcher widget must have"
318                     + " LauncherAppWidgetProviderInfo");
319         }
320         return info;
321     }
322 
323     @Override
onTouchComplete()324     public void onTouchComplete() {
325         if (!mLongPressHelper.hasPerformedLongPress()) {
326             // If a long press has been performed, we don't want to clear the record of that since
327             // we still may be receiving a touch up which we want to intercept
328             mLongPressHelper.cancelLongPress();
329         }
330     }
331 
switchToErrorView()332     public void switchToErrorView() {
333         // Update the widget with 0 Layout id, to reset the view to error view.
334         updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
335     }
336 
337     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)338     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
339         try {
340             super.onLayout(changed, left, top, right, bottom);
341         } catch (final RuntimeException e) {
342             post(new Runnable() {
343                 @Override
344                 public void run() {
345                     switchToErrorView();
346                 }
347             });
348         }
349 
350         mIsScrollable = checkScrollableRecursively(this);
351         updateColorExtraction();
352 
353         enforceRoundedCorners();
354     }
355 
356     /** Starts the drag mode. */
startDrag(AppWidgetHostViewDragListener dragListener)357     public void startDrag(AppWidgetHostViewDragListener dragListener) {
358         mIsInDragMode = true;
359         mDragListener = dragListener;
360     }
361 
362     /** Handles a drag event occurred on a workspace page, {@code pageId}. */
handleDrag(Rect rectInDragLayer, int pageId)363     public void handleDrag(Rect rectInDragLayer, int pageId) {
364         mWidgetSizeAtDrag.set(rectInDragLayer);
365         updateColorExtraction(mWidgetSizeAtDrag, pageId);
366     }
367 
368     /** Ends the drag mode. */
endDrag()369     public void endDrag() {
370         mIsInDragMode = false;
371         mDragListener = null;
372         mWidgetSizeAtDrag.setEmpty();
373     }
374 
375     /**
376      * @param rectInDragLayer Rect of widget in drag layer coordinates.
377      * @param pageId The workspace page the widget is on.
378      */
updateColorExtraction(Rect rectInDragLayer, int pageId)379     private void updateColorExtraction(Rect rectInDragLayer, int pageId) {
380         if (!mEnableColorExtraction) return;
381         mColorExtractor.getExtractedRectForViewRect(mLauncher, pageId, rectInDragLayer, mTempRectF);
382 
383         if (mTempRectF.isEmpty()) {
384             return;
385         }
386         if (!isSameLocation(mTempRectF, mLastLocationRegistered, /* epsilon= */ 1e-6f)) {
387             if (mLastLocationRegistered != null) {
388                 mColorExtractor.removeLocations();
389             }
390             mLastLocationRegistered = new RectF(mTempRectF);
391             mColorExtractor.addLocation(List.of(mLastLocationRegistered));
392         }
393     }
394 
395     /**
396      * Update the color extraction, using the current position of the app widget.
397      */
updateColorExtraction()398     private void updateColorExtraction() {
399         if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
400             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
401             mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize);
402             updateColorExtraction(mCurrentWidgetSize,
403                     mWorkspace.getPageIndexForScreenId(info.screenId));
404         }
405     }
406 
407     /**
408      * Enables the local color extraction.
409      *
410      * @param updateColors If true, this will update the color extraction using the current location
411      *                    of the App Widget.
412      */
enableColorExtraction(boolean updateColors)413     public void enableColorExtraction(boolean updateColors) {
414         mEnableColorExtraction = true;
415         if (updateColors) {
416             updateColorExtraction();
417         }
418     }
419 
420     /**
421      * Disables the local color extraction.
422      */
disableColorExtraction()423     public void disableColorExtraction() {
424         mEnableColorExtraction = false;
425     }
426 
427     // Compare two location rectangles. Locations are always in the [0;1] range.
isSameLocation(@onNull RectF rect1, @Nullable RectF rect2, float epsilon)428     private static boolean isSameLocation(@NonNull RectF rect1, @Nullable RectF rect2,
429             float epsilon) {
430         if (rect2 == null) return false;
431         return isSameCoordinate(rect1.left, rect2.left, epsilon)
432                 && isSameCoordinate(rect1.right, rect2.right, epsilon)
433                 && isSameCoordinate(rect1.top, rect2.top, epsilon)
434                 && isSameCoordinate(rect1.bottom, rect2.bottom, epsilon);
435     }
436 
isSameCoordinate(float c1, float c2, float epsilon)437     private static boolean isSameCoordinate(float c1, float c2, float epsilon) {
438         return Math.abs(c1 - c2) < epsilon;
439     }
440 
441     @Override
onColorsChanged(RectF rectF, SparseIntArray colors)442     public void onColorsChanged(RectF rectF, SparseIntArray colors) {
443         synchronized (mUpdateLock) {
444             if (isDeferringUpdates()) {
445                 mDeferredColorChange = colors;
446                 mHasDeferredColorChange = true;
447                 return;
448             }
449             mDeferredColorChange = null;
450             mHasDeferredColorChange = false;
451         }
452 
453         // setColorResources will reapply the view, which must happen in the UI thread.
454         post(() -> setColorResources(colors));
455     }
456 
457     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)458     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
459         super.onInitializeAccessibilityNodeInfo(info);
460         info.setClassName(getClass().getName());
461     }
462 
463     @Override
onWindowVisibilityChanged(int visibility)464     protected void onWindowVisibilityChanged(int visibility) {
465         super.onWindowVisibilityChanged(visibility);
466         maybeRegisterAutoAdvance();
467     }
468 
checkIfAutoAdvance()469     private void checkIfAutoAdvance() {
470         boolean isAutoAdvance = false;
471         Advanceable target = getAdvanceable();
472         if (target != null) {
473             isAutoAdvance = true;
474             target.fyiWillBeAdvancedByHostKThx();
475         }
476 
477         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
478         if (isAutoAdvance != wasAutoAdvance) {
479             if (isAutoAdvance) {
480                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
481             } else {
482                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
483             }
484             maybeRegisterAutoAdvance();
485         }
486     }
487 
getAdvanceable()488     private Advanceable getAdvanceable() {
489         AppWidgetProviderInfo info = getAppWidgetInfo();
490         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
491             return null;
492         }
493         View v = findViewById(info.autoAdvanceViewId);
494         return (v instanceof Advanceable) ? (Advanceable) v : null;
495     }
496 
maybeRegisterAutoAdvance()497     private void maybeRegisterAutoAdvance() {
498         Handler handler = getHandler();
499         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
500                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
501         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
502             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
503             if (mAutoAdvanceRunnable == null) {
504                 mAutoAdvanceRunnable = this::runAutoAdvance;
505             }
506 
507             handler.removeCallbacks(mAutoAdvanceRunnable);
508             scheduleNextAdvance();
509         }
510     }
511 
scheduleNextAdvance()512     private void scheduleNextAdvance() {
513         if (!mIsAutoAdvanceRegistered) {
514             return;
515         }
516         long now = SystemClock.uptimeMillis();
517         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
518                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
519         Handler handler = getHandler();
520         if (handler != null) {
521             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
522         }
523     }
524 
runAutoAdvance()525     private void runAutoAdvance() {
526         Advanceable target = getAdvanceable();
527         if (target != null) {
528             target.advance();
529         }
530         scheduleNextAdvance();
531     }
532 
533     @Override
onConfigurationChanged(Configuration newConfig)534     protected void onConfigurationChanged(Configuration newConfig) {
535         super.onConfigurationChanged(newConfig);
536 
537         // Only reinflate when the final configuration is same as the required configuration
538         if (mReinflateOnConfigChange && isSameOrientation()) {
539             mReinflateOnConfigChange = false;
540             reInflate();
541         }
542     }
543 
reInflate()544     public void reInflate() {
545         if (!isAttachedToWindow()) {
546             return;
547         }
548         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
549         if (info == null) {
550             // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
551             return;
552         }
553         // Remove and rebind the current widget (which was inflated in the wrong
554         // orientation), but don't delete it from the database
555         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
556         mLauncher.bindAppWidget(info);
557     }
558 
559     @Override
shouldAllowDirectClick()560     protected boolean shouldAllowDirectClick() {
561         if (getTag() instanceof ItemInfo) {
562             ItemInfo item = (ItemInfo) getTag();
563             return item.spanX == 1 && item.spanY == 1;
564         }
565         return false;
566     }
567 
568     @UiThread
resetRoundedCorners()569     private void resetRoundedCorners() {
570         setOutlineProvider(ViewOutlineProvider.BACKGROUND);
571         setClipToOutline(false);
572     }
573 
574     @UiThread
enforceRoundedCorners()575     private void enforceRoundedCorners() {
576         if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
577             resetRoundedCorners();
578             return;
579         }
580         View background = RoundedCornerEnforcement.findBackground(this);
581         if (background == null
582                 || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
583             resetRoundedCorners();
584             return;
585         }
586         RoundedCornerEnforcement.computeRoundedRectangle(this,
587                 background,
588                 mEnforcedRectangle);
589         setOutlineProvider(mCornerRadiusEnforcementOutline);
590         setClipToOutline(true);
591     }
592 
593     /** Returns the corner radius currently enforced, in pixels. */
getEnforcedCornerRadius()594     public float getEnforcedCornerRadius() {
595         return mEnforcedCornerRadius;
596     }
597 
598     /** Returns true if the corner radius are enforced for this App Widget. */
hasEnforcedCornerRadius()599     public boolean hasEnforcedCornerRadius() {
600         return getClipToOutline();
601     }
602 
603 }
604