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