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.Rect;
23 import android.os.Handler;
24 import android.os.SystemClock;
25 import android.util.SparseBooleanArray;
26 import android.util.SparseIntArray;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewDebug;
30 import android.view.ViewGroup;
31 import android.view.accessibility.AccessibilityNodeInfo;
32 import android.widget.AdapterView;
33 import android.widget.Advanceable;
34 import android.widget.RemoteViews;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.launcher3.CheckLongPressHelper;
39 import com.android.launcher3.Launcher;
40 import com.android.launcher3.R;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.dragndrop.DragLayer;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
45 import com.android.launcher3.util.Themes;
46 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
47 
48 /**
49  * {@inheritDoc}
50  */
51 public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
52         implements TouchCompleteListener, View.OnLongClickListener,
53         LocalColorExtractor.Listener {
54 
55     // Related to the auto-advancing of widgets
56     private static final long ADVANCE_INTERVAL = 20000;
57     private static final long ADVANCE_STAGGER = 250;
58 
59     // Maintains a list of widget ids which are supposed to be auto advanced.
60     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
61     // Maximum duration for which updates can be deferred.
62     private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
63 
64     private final Rect mTempRect = new Rect();
65     private final CheckLongPressHelper mLongPressHelper;
66     protected final Launcher mLauncher;
67 
68     @ViewDebug.ExportedProperty(category = "launcher")
69     private boolean mReinflateOnConfigChange;
70 
71     // Maintain the color manager.
72     private final LocalColorExtractor mColorExtractor;
73 
74     private boolean mIsScrollable;
75     private boolean mIsAttachedToWindow;
76     private boolean mIsAutoAdvanceRegistered;
77     private Runnable mAutoAdvanceRunnable;
78 
79     private long mDeferUpdatesUntilMillis = 0;
80     private RemoteViews mDeferredRemoteViews;
81     private boolean mHasDeferredColorChange = false;
82     private @Nullable SparseIntArray mDeferredColorChange = null;
83 
84     // The following member variables are only used during drag-n-drop.
85     private boolean mIsInDragMode = false;
86     /** The drag content width which is only set when the drag content scale is not 1f. */
87     private int mDragContentWidth = 0;
88     /** The drag content height which is only set when the drag content scale is not 1f. */
89     private int mDragContentHeight = 0;
90 
LauncherAppWidgetHostView(Context context)91     public LauncherAppWidgetHostView(Context context) {
92         super(context);
93         mLauncher = Launcher.getLauncher(context);
94         mLongPressHelper = new CheckLongPressHelper(this, this);
95         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
96         setBackgroundResource(R.drawable.widget_internal_focus_bg);
97 
98         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
99             setOnLightBackground(true);
100         }
101         mColorExtractor = LocalColorExtractor.newInstance(getContext());
102     }
103 
104     @Override
setColorResources(@ullable SparseIntArray colors)105     public void setColorResources(@Nullable SparseIntArray colors) {
106         if (colors == null) {
107             resetColorResources();
108         } else {
109             super.setColorResources(colors);
110         }
111     }
112 
113     @Override
onLongClick(View view)114     public boolean onLongClick(View view) {
115         if (mIsScrollable) {
116             DragLayer dragLayer = mLauncher.getDragLayer();
117             dragLayer.requestDisallowInterceptTouchEvent(false);
118         }
119         view.performLongClick();
120         return true;
121     }
122 
123     @Override
updateAppWidget(RemoteViews remoteViews)124     public void updateAppWidget(RemoteViews remoteViews) {
125         if (isDeferringUpdates()) {
126             mDeferredRemoteViews = remoteViews;
127             return;
128         }
129         mDeferredRemoteViews = null;
130 
131         super.updateAppWidget(remoteViews);
132 
133         // The provider info or the views might have changed.
134         checkIfAutoAdvance();
135 
136         // It is possible that widgets can receive updates while launcher is not in the foreground.
137         // Consequently, the widgets will be inflated for the orientation of the foreground activity
138         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
139         // orientation.
140         mReinflateOnConfigChange = !isSameOrientation();
141     }
142 
isSameOrientation()143     private boolean isSameOrientation() {
144         return mLauncher.getResources().getConfiguration().orientation ==
145                 mLauncher.getOrientation();
146     }
147 
checkScrollableRecursively(ViewGroup viewGroup)148     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
149         if (viewGroup instanceof AdapterView) {
150             return true;
151         } else {
152             for (int i = 0; i < viewGroup.getChildCount(); i++) {
153                 View child = viewGroup.getChildAt(i);
154                 if (child instanceof ViewGroup) {
155                     if (checkScrollableRecursively((ViewGroup) child)) {
156                         return true;
157                     }
158                 }
159             }
160         }
161         return false;
162     }
163 
164     /**
165      * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and
166      * colors through {@link #onColorsChanged} are currently being deferred.
167      * @see #beginDeferringUpdates()
168      */
isDeferringUpdates()169     private boolean isDeferringUpdates() {
170         return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis;
171     }
172 
173     /**
174      * Begin deferring the application of any {@link RemoteViews} updates made through
175      * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until
176      * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or
177      * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed.
178      */
beginDeferringUpdates()179     public void beginDeferringUpdates() {
180         mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
181     }
182 
183     /**
184      * Stop deferring the application of {@link RemoteViews} updates made through
185      * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply
186      * any deferred updates.
187      */
endDeferringUpdates()188     public void endDeferringUpdates() {
189         RemoteViews remoteViews;
190         SparseIntArray deferredColors;
191         boolean hasDeferredColors;
192         mDeferUpdatesUntilMillis = 0;
193         remoteViews = mDeferredRemoteViews;
194         mDeferredRemoteViews = null;
195         deferredColors = mDeferredColorChange;
196         hasDeferredColors = mHasDeferredColorChange;
197         mDeferredColorChange = null;
198         mHasDeferredColorChange = false;
199 
200         if (remoteViews != null) {
201             updateAppWidget(remoteViews);
202         }
203         if (hasDeferredColors) {
204             onColorsChanged(deferredColors);
205         }
206     }
207 
onInterceptTouchEvent(MotionEvent ev)208     public boolean onInterceptTouchEvent(MotionEvent ev) {
209         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
210             DragLayer dragLayer = mLauncher.getDragLayer();
211             if (mIsScrollable) {
212                 dragLayer.requestDisallowInterceptTouchEvent(true);
213             }
214             dragLayer.setTouchCompleteListener(this);
215         }
216         mLongPressHelper.onTouchEvent(ev);
217         return mLongPressHelper.hasPerformedLongPress();
218     }
219 
onTouchEvent(MotionEvent ev)220     public boolean onTouchEvent(MotionEvent ev) {
221         mLongPressHelper.onTouchEvent(ev);
222         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
223         return true;
224     }
225 
226     @Override
onAttachedToWindow()227     protected void onAttachedToWindow() {
228         super.onAttachedToWindow();
229         mIsAttachedToWindow = true;
230         checkIfAutoAdvance();
231         mColorExtractor.setListener(this);
232     }
233 
234     @Override
onDetachedFromWindow()235     protected void onDetachedFromWindow() {
236         super.onDetachedFromWindow();
237 
238         // We can't directly use isAttachedToWindow() here, as this is called before the internal
239         // state is updated. So isAttachedToWindow() will return true until next frame.
240         mIsAttachedToWindow = false;
241         checkIfAutoAdvance();
242         mColorExtractor.setListener(null);
243     }
244 
245     @Override
cancelLongPress()246     public void cancelLongPress() {
247         super.cancelLongPress();
248         mLongPressHelper.cancelLongPress();
249     }
250 
251     @Override
getAppWidgetInfo()252     public AppWidgetProviderInfo getAppWidgetInfo() {
253         AppWidgetProviderInfo info = super.getAppWidgetInfo();
254         if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
255             throw new IllegalStateException("Launcher widget must have"
256                     + " LauncherAppWidgetProviderInfo");
257         }
258         return info;
259     }
260 
261     @Override
onTouchComplete()262     public void onTouchComplete() {
263         if (!mLongPressHelper.hasPerformedLongPress()) {
264             // If a long press has been performed, we don't want to clear the record of that since
265             // we still may be receiving a touch up which we want to intercept
266             mLongPressHelper.cancelLongPress();
267         }
268     }
269 
270     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)271     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
272         super.onLayout(changed, left, top, right, bottom);
273         mIsScrollable = checkScrollableRecursively(this);
274 
275         if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
276             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
277             mTempRect.set(left, top, right, bottom);
278             mColorExtractor.setWorkspaceLocation(mTempRect, (View) getParent(), info.screenId);
279         }
280     }
281 
282     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)283     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
284         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
285         if (mIsInDragMode && mDragContentWidth > 0 && mDragContentHeight > 0
286                 && getChildCount() == 1) {
287             measureChild(getChildAt(0), MeasureSpec.getSize(mDragContentWidth),
288                     MeasureSpec.getSize(mDragContentHeight));
289         }
290     }
291 
292     /** Starts the drag mode. */
startDrag()293     public void startDrag() {
294         mIsInDragMode = true;
295         // In the case of dragging a scaled preview from widgets picker, we should reuse the
296         // previously measured dimension from WidgetCell#measureAndComputeWidgetPreviewScale, which
297         // measures the dimension of a widget preview without its parent's bound before scaling
298         // down.
299         if ((getScaleX() != 1f || getScaleY() != 1f) && getChildCount() == 1) {
300             mDragContentWidth = getChildAt(0).getMeasuredWidth();
301             mDragContentHeight = getChildAt(0).getMeasuredHeight();
302         }
303     }
304 
305     /** Handles a drag event occurred on a workspace page corresponding to the {@code screenId}. */
handleDrag(Rect rectInView, View view, int screenId)306     public void handleDrag(Rect rectInView, View view, int screenId) {
307         if (mIsInDragMode) {
308             mColorExtractor.setWorkspaceLocation(rectInView, view, screenId);
309         }
310     }
311 
312     /** Ends the drag mode. */
endDrag()313     public void endDrag() {
314         mIsInDragMode = false;
315         mDragContentWidth = 0;
316         mDragContentHeight = 0;
317         requestLayout();
318     }
319 
320     @Override
onColorsChanged(SparseIntArray colors)321     public void onColorsChanged(SparseIntArray colors) {
322         if (isDeferringUpdates()) {
323             mDeferredColorChange = colors;
324             mHasDeferredColorChange = true;
325             return;
326         }
327         mDeferredColorChange = null;
328         mHasDeferredColorChange = false;
329 
330         // setColorResources will reapply the view, which must happen in the UI thread.
331         post(() -> setColorResources(colors));
332     }
333 
334     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)335     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
336         super.onInitializeAccessibilityNodeInfo(info);
337         info.setClassName(getClass().getName());
338     }
339 
340     @Override
onWindowVisibilityChanged(int visibility)341     protected void onWindowVisibilityChanged(int visibility) {
342         super.onWindowVisibilityChanged(visibility);
343         maybeRegisterAutoAdvance();
344     }
345 
checkIfAutoAdvance()346     private void checkIfAutoAdvance() {
347         boolean isAutoAdvance = false;
348         Advanceable target = getAdvanceable();
349         if (target != null) {
350             isAutoAdvance = true;
351             target.fyiWillBeAdvancedByHostKThx();
352         }
353 
354         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
355         if (isAutoAdvance != wasAutoAdvance) {
356             if (isAutoAdvance) {
357                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
358             } else {
359                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
360             }
361             maybeRegisterAutoAdvance();
362         }
363     }
364 
getAdvanceable()365     private Advanceable getAdvanceable() {
366         AppWidgetProviderInfo info = getAppWidgetInfo();
367         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
368             return null;
369         }
370         View v = findViewById(info.autoAdvanceViewId);
371         return (v instanceof Advanceable) ? (Advanceable) v : null;
372     }
373 
maybeRegisterAutoAdvance()374     private void maybeRegisterAutoAdvance() {
375         Handler handler = getHandler();
376         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
377                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
378         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
379             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
380             if (mAutoAdvanceRunnable == null) {
381                 mAutoAdvanceRunnable = this::runAutoAdvance;
382             }
383 
384             handler.removeCallbacks(mAutoAdvanceRunnable);
385             scheduleNextAdvance();
386         }
387     }
388 
scheduleNextAdvance()389     private void scheduleNextAdvance() {
390         if (!mIsAutoAdvanceRegistered) {
391             return;
392         }
393         long now = SystemClock.uptimeMillis();
394         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
395                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
396         Handler handler = getHandler();
397         if (handler != null) {
398             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
399         }
400     }
401 
runAutoAdvance()402     private void runAutoAdvance() {
403         Advanceable target = getAdvanceable();
404         if (target != null) {
405             target.advance();
406         }
407         scheduleNextAdvance();
408     }
409 
410     @Override
onConfigurationChanged(Configuration newConfig)411     protected void onConfigurationChanged(Configuration newConfig) {
412         super.onConfigurationChanged(newConfig);
413 
414         // Only reinflate when the final configuration is same as the required configuration
415         if (mReinflateOnConfigChange && isSameOrientation()) {
416             mReinflateOnConfigChange = false;
417             reInflate();
418         }
419     }
420 
reInflate()421     public void reInflate() {
422         if (!isAttachedToWindow()) {
423             return;
424         }
425         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
426         if (info == null) {
427             // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
428             return;
429         }
430         // Remove and rebind the current widget (which was inflated in the wrong
431         // orientation), but don't delete it from the database
432         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
433         mLauncher.bindAppWidget(info);
434     }
435 
436     @Override
shouldAllowDirectClick()437     protected boolean shouldAllowDirectClick() {
438         if (getTag() instanceof ItemInfo) {
439             ItemInfo item = (ItemInfo) getTag();
440             return item.spanX == 1 && item.spanY == 1;
441         }
442         return false;
443     }
444 }
445