• 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.graphics.Rect;
22 import android.os.Handler;
23 import android.os.Parcelable;
24 import android.os.SystemClock;
25 import android.os.Trace;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.util.SparseBooleanArray;
29 import android.util.SparseIntArray;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.accessibility.AccessibilityNodeInfo;
34 import android.widget.AdapterView;
35 import android.widget.Advanceable;
36 import android.widget.RemoteViews;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import com.android.launcher3.CheckLongPressHelper;
42 import com.android.launcher3.Flags;
43 import com.android.launcher3.R;
44 import com.android.launcher3.model.data.ItemInfo;
45 import com.android.launcher3.util.Themes;
46 import com.android.launcher3.views.ActivityContext;
47 import com.android.launcher3.views.BaseDragLayer;
48 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
49 
50 /**
51  * {@inheritDoc}
52  */
53 public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
54         implements TouchCompleteListener, View.OnLongClickListener {
55 
56     private static final String TAG = "LauncherAppWidgetHostView";
57 
58     // Related to the auto-advancing of widgets
59     private static final long ADVANCE_INTERVAL = 20000;
60     private static final long ADVANCE_STAGGER = 250;
61 
62     private @Nullable CellChildViewPreLayoutListener mCellChildViewPreLayoutListener;
63 
64     // Maintains a list of widget ids which are supposed to be auto advanced.
65     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
66     // Maximum duration for which updates can be deferred.
67     private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
68 
69     private static final String TRACE_METHOD_NAME = "appwidget load-widget ";
70 
71     private static final Integer NO_LAYOUT_ID = Integer.valueOf(0);
72 
73     private final CheckLongPressHelper mLongPressHelper;
74     protected final ActivityContext mActivityContext;
75 
76     private boolean mIsScrollable;
77     private boolean mIsAttachedToWindow;
78     private boolean mIsAutoAdvanceRegistered;
79     private Runnable mAutoAdvanceRunnable;
80 
81     private long mDeferUpdatesUntilMillis = 0;
82     private RemoteViews mLastRemoteViews;
83     private boolean mReapplyOnResumeUpdates = false;
84 
85     private boolean mTrackingWidgetUpdate = false;
86 
87     private int mFocusRectOutsets = 0;
88 
LauncherAppWidgetHostView(Context context)89     public LauncherAppWidgetHostView(Context context) {
90         super(context);
91         mActivityContext = ActivityContext.lookupContext(context);
92         mLongPressHelper = new CheckLongPressHelper(this, this);
93         setAccessibilityDelegate(mActivityContext.getAccessibilityDelegate());
94         setBackgroundResource(R.drawable.widget_internal_focus_bg);
95         if (Flags.enableFocusOutline()) {
96             setDefaultFocusHighlightEnabled(false);
97             mFocusRectOutsets = context.getResources().getDimensionPixelSize(
98                     R.dimen.focus_rect_widget_outsets);
99         }
100 
101         if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) {
102             setOnLightBackground(true);
103         }
104     }
105 
106     @Override
setColorResources(@ullable SparseIntArray colors)107     public void setColorResources(@Nullable SparseIntArray colors) {
108         if (colors == null) {
109             resetColorResources();
110         } else {
111             super.setColorResources(colors);
112         }
113     }
114 
115     @Override
onLongClick(View view)116     public boolean onLongClick(View view) {
117         if (mIsScrollable) {
118             mActivityContext.getDragLayer().requestDisallowInterceptTouchEvent(false);
119         }
120         view.performLongClick();
121         return true;
122     }
123 
124     @Override
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)125     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
126         super.setAppWidget(appWidgetId, info);
127         if (!mTrackingWidgetUpdate && appWidgetId != -1) {
128             mTrackingWidgetUpdate = true;
129             Trace.beginAsyncSection(TRACE_METHOD_NAME + info.provider, appWidgetId);
130             Log.i(TAG, "App widget created with id: " + appWidgetId);
131         }
132     }
133 
134     @Override
updateAppWidget(RemoteViews remoteViews)135     public void updateAppWidget(RemoteViews remoteViews) {
136         if (mTrackingWidgetUpdate && remoteViews != null) {
137             Log.i(TAG, "App widget with id: " + getAppWidgetId() + " loaded");
138             Trace.endAsyncSection(
139                     TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId());
140             mTrackingWidgetUpdate = false;
141         }
142         mLastRemoteViews = remoteViews;
143         mReapplyOnResumeUpdates = isDeferringUpdates();
144         if (mReapplyOnResumeUpdates) {
145             return;
146         }
147 
148         super.updateAppWidget(remoteViews);
149 
150         // The provider info or the views might have changed.
151         checkIfAutoAdvance();
152     }
153 
154     @Override
onViewAdded(View child)155     public void onViewAdded(View child) {
156         super.onViewAdded(child);
157         mReapplyOnResumeUpdates |= isDeferringUpdates();
158     }
159 
160     @Override
onViewRemoved(View child)161     public void onViewRemoved(View child) {
162         super.onViewRemoved(child);
163         mReapplyOnResumeUpdates |= isDeferringUpdates();
164     }
165 
checkScrollableRecursively(ViewGroup viewGroup)166     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
167         if (viewGroup instanceof AdapterView) {
168             return true;
169         } else {
170             for (int i = 0; i < viewGroup.getChildCount(); i++) {
171                 View child = viewGroup.getChildAt(i);
172                 if (child instanceof ViewGroup) {
173                     if (checkScrollableRecursively((ViewGroup) child)) {
174                         return true;
175                     }
176                 }
177             }
178         }
179         return false;
180     }
181 
isTaggedAsScrollable()182     private boolean isTaggedAsScrollable() {
183         // TODO: Introduce new api in AppWidgetHostView to indicate whether the widget is
184         // scrollable.
185         for (int i = 0; i < this.getChildCount(); i++) {
186             View child = this.getChildAt(i);
187             final Integer layoutId = (Integer) child.getTag(android.R.id.widget_frame);
188             if (layoutId != null) {
189                 // The layout id is only set to 0 when RemoteViews is created from
190                 // DrawInstructions.
191                 return NO_LAYOUT_ID.equals(layoutId);
192             }
193         }
194         return false;
195     }
196 
197     /**
198      * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} are
199      * currently being deferred.
200      * @see #beginDeferringUpdates()
201      */
isDeferringUpdates()202     private boolean isDeferringUpdates() {
203         return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis;
204     }
205 
206     /**
207      * Begin deferring the application of any {@link RemoteViews} updates made through
208      * {@link #updateAppWidget} until {@link #endDeferringUpdates()} has been called or the next
209      * {@link #updateAppWidget} 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 apply any deferred updates.
218      */
endDeferringUpdates()219     public void endDeferringUpdates() {
220         mDeferUpdatesUntilMillis = 0;
221         if (mReapplyOnResumeUpdates) {
222             updateAppWidget(mLastRemoteViews);
223         }
224     }
225 
226     @Override
onInterceptTouchEvent(MotionEvent ev)227     public boolean onInterceptTouchEvent(MotionEvent ev) {
228         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
229             BaseDragLayer<?> dragLayer = mActivityContext.getDragLayer();
230             if (mIsScrollable) {
231                 dragLayer.requestDisallowInterceptTouchEvent(true);
232             }
233             dragLayer.setTouchCompleteListener(this);
234         }
235         mLongPressHelper.onTouchEvent(ev);
236         return mLongPressHelper.hasPerformedLongPress();
237     }
238 
239     @Override
onTouchEvent(MotionEvent ev)240     public boolean onTouchEvent(MotionEvent ev) {
241         mLongPressHelper.onTouchEvent(ev);
242         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
243         return true;
244     }
245 
246     @Override
onAttachedToWindow()247     protected void onAttachedToWindow() {
248         super.onAttachedToWindow();
249         mIsAttachedToWindow = true;
250         checkIfAutoAdvance();
251     }
252 
253     @Override
onDetachedFromWindow()254     protected void onDetachedFromWindow() {
255         super.onDetachedFromWindow();
256 
257         // We can't directly use isAttachedToWindow() here, as this is called before the internal
258         // state is updated. So isAttachedToWindow() will return true until next frame.
259         mIsAttachedToWindow = false;
260         checkIfAutoAdvance();
261     }
262 
263     @Override
cancelLongPress()264     public void cancelLongPress() {
265         super.cancelLongPress();
266         mLongPressHelper.cancelLongPress();
267     }
268 
269     @Override
getFocusedRect(Rect r)270     public void getFocusedRect(Rect r) {
271         super.getFocusedRect(r);
272         // Outset to a larger rect for drawing a padding between focus outline and widget
273         r.inset(mFocusRectOutsets, mFocusRectOutsets);
274     }
275 
276     @Override
onTouchComplete()277     public void onTouchComplete() {
278         if (!mLongPressHelper.hasPerformedLongPress()) {
279             // If a long press has been performed, we don't want to clear the record of that since
280             // we still may be receiving a touch up which we want to intercept
281             mLongPressHelper.cancelLongPress();
282         }
283     }
284 
285     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)286     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
287         super.onLayout(changed, left, top, right, bottom);
288         mIsScrollable = isTaggedAsScrollable() || checkScrollableRecursively(this);
289     }
290 
291     /**
292      * Set the pre-layout listener
293      * @param listener The listener to be notified when {@code CellLayout} is to layout this view
294      */
setCellChildViewPreLayoutListener( @onNull CellChildViewPreLayoutListener listener)295     public void setCellChildViewPreLayoutListener(
296             @NonNull CellChildViewPreLayoutListener listener) {
297         mCellChildViewPreLayoutListener = listener;
298     }
299 
300     /** @return The current cell layout listener */
301     @Nullable
getCellChildViewPreLayoutListener()302     public CellChildViewPreLayoutListener getCellChildViewPreLayoutListener() {
303         return mCellChildViewPreLayoutListener;
304     }
305 
306     /** Clear the listener for the pre-layout in CellLayout */
clearCellChildViewPreLayoutListener()307     public void clearCellChildViewPreLayoutListener() {
308         mCellChildViewPreLayoutListener = null;
309     }
310 
311     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)312     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
313         super.onInitializeAccessibilityNodeInfo(info);
314         info.setClassName(getClass().getName());
315     }
316 
317     @Override
onWindowVisibilityChanged(int visibility)318     protected void onWindowVisibilityChanged(int visibility) {
319         super.onWindowVisibilityChanged(visibility);
320         maybeRegisterAutoAdvance();
321     }
322 
checkIfAutoAdvance()323     private void checkIfAutoAdvance() {
324         boolean isAutoAdvance = false;
325         Advanceable target = getAdvanceable();
326         if (target != null) {
327             isAutoAdvance = true;
328             target.fyiWillBeAdvancedByHostKThx();
329         }
330 
331         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
332         if (isAutoAdvance != wasAutoAdvance) {
333             if (isAutoAdvance) {
334                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
335             } else {
336                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
337             }
338             maybeRegisterAutoAdvance();
339         }
340     }
341 
getAdvanceable()342     private Advanceable getAdvanceable() {
343         AppWidgetProviderInfo info = getAppWidgetInfo();
344         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
345             return null;
346         }
347         View v = findViewById(info.autoAdvanceViewId);
348         return (v instanceof Advanceable) ? (Advanceable) v : null;
349     }
350 
maybeRegisterAutoAdvance()351     private void maybeRegisterAutoAdvance() {
352         Handler handler = getHandler();
353         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
354                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
355         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
356             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
357             if (mAutoAdvanceRunnable == null) {
358                 mAutoAdvanceRunnable = this::runAutoAdvance;
359             }
360 
361             handler.removeCallbacks(mAutoAdvanceRunnable);
362             scheduleNextAdvance();
363         }
364     }
365 
scheduleNextAdvance()366     private void scheduleNextAdvance() {
367         if (!mIsAutoAdvanceRegistered) {
368             return;
369         }
370         long now = SystemClock.uptimeMillis();
371         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
372                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
373         Handler handler = getHandler();
374         if (handler != null) {
375             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
376         }
377     }
378 
runAutoAdvance()379     private void runAutoAdvance() {
380         Advanceable target = getAdvanceable();
381         if (target != null) {
382             target.advance();
383         }
384         scheduleNextAdvance();
385     }
386 
387     @Override
shouldAllowDirectClick()388     protected boolean shouldAllowDirectClick() {
389         if (getTag() instanceof ItemInfo item) {
390             return item.spanX == 1 && item.spanY == 1;
391         }
392         return false;
393     }
394 
395     /**
396      * Listener interface to be called when {@code CellLayout} is about to layout this child view
397      */
398     public interface CellChildViewPreLayoutListener {
399         /**
400          * Notify the bound changes to this view on pre-layout
401          * @param v The view which the listener is set for
402          * @param left The new left coordinate of this view
403          * @param top The new top coordinate of this view
404          * @param right The new right coordinate of this view
405          * @param bottom The new bottom coordinate of this view
406          */
notifyBoundChangeOnPreLayout(View v, int left, int top, int right, int bottom)407         void notifyBoundChangeOnPreLayout(View v, int left, int top, int right, int bottom);
408     }
409 
410     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)411     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
412         try {
413             super.dispatchRestoreInstanceState(container);
414         } catch (Exception e) {
415             Log.i(TAG, "Exception: " + e);
416         }
417     }
418 }
419