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.os.Handler;
23 import android.os.SystemClock;
24 import android.util.SparseBooleanArray;
25 import android.view.LayoutInflater;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewDebug;
29 import android.view.ViewGroup;
30 import android.view.accessibility.AccessibilityNodeInfo;
31 import android.widget.AdapterView;
32 import android.widget.Advanceable;
33 import android.widget.RemoteViews;
34 
35 import com.android.launcher3.CheckLongPressHelper;
36 import com.android.launcher3.Launcher;
37 import com.android.launcher3.LauncherAppWidgetProviderInfo;
38 import com.android.launcher3.R;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.dragndrop.DragLayer;
41 import com.android.launcher3.model.data.ItemInfo;
42 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
43 import com.android.launcher3.util.Executors;
44 import com.android.launcher3.util.Themes;
45 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
46 
47 /**
48  * {@inheritDoc}
49  */
50 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
51         implements TouchCompleteListener, View.OnLongClickListener {
52 
53     // Related to the auto-advancing of widgets
54     private static final long ADVANCE_INTERVAL = 20000;
55     private static final long ADVANCE_STAGGER = 250;
56 
57     // Maintains a list of widget ids which are supposed to be auto advanced.
58     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
59 
60     protected final LayoutInflater mInflater;
61 
62     private final CheckLongPressHelper mLongPressHelper;
63     protected final Launcher mLauncher;
64 
65     @ViewDebug.ExportedProperty(category = "launcher")
66     private boolean mReinflateOnConfigChange;
67 
68     private boolean mIsScrollable;
69     private boolean mIsAttachedToWindow;
70     private boolean mIsAutoAdvanceRegistered;
71     private Runnable mAutoAdvanceRunnable;
72 
73 
74 
LauncherAppWidgetHostView(Context context)75     public LauncherAppWidgetHostView(Context context) {
76         super(context);
77         mLauncher = Launcher.getLauncher(context);
78         mLongPressHelper = new CheckLongPressHelper(this, this);
79         mInflater = LayoutInflater.from(context);
80         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
81         setBackgroundResource(R.drawable.widget_internal_focus_bg);
82 
83         if (Utilities.ATLEAST_OREO) {
84             setExecutor(Executors.THREAD_POOL_EXECUTOR);
85         }
86         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
87             setOnLightBackground(true);
88         }
89     }
90 
91     @Override
onLongClick(View view)92     public boolean onLongClick(View view) {
93         if (mIsScrollable) {
94             DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
95             dragLayer.requestDisallowInterceptTouchEvent(false);
96         }
97         view.performLongClick();
98         return true;
99     }
100 
101     @Override
getErrorView()102     protected View getErrorView() {
103         return mInflater.inflate(R.layout.appwidget_error, this, false);
104     }
105 
106     @Override
updateAppWidget(RemoteViews remoteViews)107     public void updateAppWidget(RemoteViews remoteViews) {
108         super.updateAppWidget(remoteViews);
109 
110         // The provider info or the views might have changed.
111         checkIfAutoAdvance();
112 
113         // It is possible that widgets can receive updates while launcher is not in the foreground.
114         // Consequently, the widgets will be inflated for the orientation of the foreground activity
115         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
116         // orientation.
117         mReinflateOnConfigChange = !isSameOrientation();
118     }
119 
isSameOrientation()120     private boolean isSameOrientation() {
121         return mLauncher.getResources().getConfiguration().orientation ==
122                 mLauncher.getOrientation();
123     }
124 
checkScrollableRecursively(ViewGroup viewGroup)125     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
126         if (viewGroup instanceof AdapterView) {
127             return true;
128         } else {
129             for (int i=0; i < viewGroup.getChildCount(); i++) {
130                 View child = viewGroup.getChildAt(i);
131                 if (child instanceof ViewGroup) {
132                     if (checkScrollableRecursively((ViewGroup) child)) {
133                         return true;
134                     }
135                 }
136             }
137         }
138         return false;
139     }
140 
onInterceptTouchEvent(MotionEvent ev)141     public boolean onInterceptTouchEvent(MotionEvent ev) {
142         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
143             DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
144             if (mIsScrollable) {
145                 dragLayer.requestDisallowInterceptTouchEvent(true);
146             }
147             dragLayer.setTouchCompleteListener(this);
148         }
149         mLongPressHelper.onTouchEvent(ev);
150         return mLongPressHelper.hasPerformedLongPress();
151     }
152 
onTouchEvent(MotionEvent ev)153     public boolean onTouchEvent(MotionEvent ev) {
154         mLongPressHelper.onTouchEvent(ev);
155         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
156         return true;
157     }
158 
159     @Override
onAttachedToWindow()160     protected void onAttachedToWindow() {
161         super.onAttachedToWindow();
162 
163         mIsAttachedToWindow = true;
164         checkIfAutoAdvance();
165     }
166 
167     @Override
onDetachedFromWindow()168     protected void onDetachedFromWindow() {
169         super.onDetachedFromWindow();
170 
171         // We can't directly use isAttachedToWindow() here, as this is called before the internal
172         // state is updated. So isAttachedToWindow() will return true until next frame.
173         mIsAttachedToWindow = false;
174         checkIfAutoAdvance();
175     }
176 
177     @Override
cancelLongPress()178     public void cancelLongPress() {
179         super.cancelLongPress();
180         mLongPressHelper.cancelLongPress();
181     }
182 
183     @Override
getAppWidgetInfo()184     public AppWidgetProviderInfo getAppWidgetInfo() {
185         AppWidgetProviderInfo info = super.getAppWidgetInfo();
186         if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
187             throw new IllegalStateException("Launcher widget must have"
188                     + " LauncherAppWidgetProviderInfo");
189         }
190         return info;
191     }
192 
193     @Override
onTouchComplete()194     public void onTouchComplete() {
195         if (!mLongPressHelper.hasPerformedLongPress()) {
196             // If a long press has been performed, we don't want to clear the record of that since
197             // we still may be receiving a touch up which we want to intercept
198             mLongPressHelper.cancelLongPress();
199         }
200     }
201 
switchToErrorView()202     public void switchToErrorView() {
203         // Update the widget with 0 Layout id, to reset the view to error view.
204         updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
205     }
206 
207     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)208     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
209         try {
210             super.onLayout(changed, left, top, right, bottom);
211         } catch (final RuntimeException e) {
212             post(new Runnable() {
213                 @Override
214                 public void run() {
215                     switchToErrorView();
216                 }
217             });
218         }
219 
220         mIsScrollable = checkScrollableRecursively(this);
221     }
222 
223     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)224     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
225         super.onInitializeAccessibilityNodeInfo(info);
226         info.setClassName(getClass().getName());
227     }
228 
229     @Override
onWindowVisibilityChanged(int visibility)230     protected void onWindowVisibilityChanged(int visibility) {
231         super.onWindowVisibilityChanged(visibility);
232         maybeRegisterAutoAdvance();
233     }
234 
checkIfAutoAdvance()235     private void checkIfAutoAdvance() {
236         boolean isAutoAdvance = false;
237         Advanceable target = getAdvanceable();
238         if (target != null) {
239             isAutoAdvance = true;
240             target.fyiWillBeAdvancedByHostKThx();
241         }
242 
243         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
244         if (isAutoAdvance != wasAutoAdvance) {
245             if (isAutoAdvance) {
246                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
247             } else {
248                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
249             }
250             maybeRegisterAutoAdvance();
251         }
252     }
253 
getAdvanceable()254     private Advanceable getAdvanceable() {
255         AppWidgetProviderInfo info = getAppWidgetInfo();
256         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
257             return null;
258         }
259         View v = findViewById(info.autoAdvanceViewId);
260         return (v instanceof Advanceable) ? (Advanceable) v : null;
261     }
262 
maybeRegisterAutoAdvance()263     private void maybeRegisterAutoAdvance() {
264         Handler handler = getHandler();
265         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
266                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
267         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
268             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
269             if (mAutoAdvanceRunnable == null) {
270                 mAutoAdvanceRunnable = this::runAutoAdvance;
271             }
272 
273             handler.removeCallbacks(mAutoAdvanceRunnable);
274             scheduleNextAdvance();
275         }
276     }
277 
scheduleNextAdvance()278     private void scheduleNextAdvance() {
279         if (!mIsAutoAdvanceRegistered) {
280             return;
281         }
282         long now = SystemClock.uptimeMillis();
283         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
284                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
285         Handler handler = getHandler();
286         if (handler != null) {
287             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
288         }
289     }
290 
runAutoAdvance()291     private void runAutoAdvance() {
292         Advanceable target = getAdvanceable();
293         if (target != null) {
294             target.advance();
295         }
296         scheduleNextAdvance();
297     }
298 
299     @Override
onConfigurationChanged(Configuration newConfig)300     protected void onConfigurationChanged(Configuration newConfig) {
301         super.onConfigurationChanged(newConfig);
302 
303         // Only reinflate when the final configuration is same as the required configuration
304         if (mReinflateOnConfigChange && isSameOrientation()) {
305             mReinflateOnConfigChange = false;
306             reInflate();
307         }
308     }
309 
reInflate()310     public void reInflate() {
311         if (!isAttachedToWindow()) {
312             return;
313         }
314         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
315         // Remove and rebind the current widget (which was inflated in the wrong
316         // orientation), but don't delete it from the database
317         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
318         mLauncher.bindAppWidget(info);
319     }
320 
321     @Override
shouldAllowDirectClick()322     protected boolean shouldAllowDirectClick() {
323         if (getTag() instanceof ItemInfo) {
324             ItemInfo item = (ItemInfo) getTag();
325             return item.spanX == 1 && item.spanY == 1;
326         }
327         return false;
328     }
329 }
330