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