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