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.graphics.PointF;
23 import android.os.Handler;
24 import android.os.SystemClock;
25 import android.util.SparseBooleanArray;
26 import android.view.LayoutInflater;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewConfiguration;
30 import android.view.ViewDebug;
31 import android.view.ViewGroup;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 import android.widget.AdapterView;
34 import android.widget.Advanceable;
35 import android.widget.RemoteViews;
36 
37 import com.android.launcher3.CheckLongPressHelper;
38 import com.android.launcher3.ItemInfo;
39 import com.android.launcher3.Launcher;
40 import com.android.launcher3.LauncherAppWidgetInfo;
41 import com.android.launcher3.LauncherAppWidgetProviderInfo;
42 import com.android.launcher3.R;
43 import com.android.launcher3.SimpleOnStylusPressListener;
44 import com.android.launcher3.StylusEventHelper;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.dragndrop.DragLayer;
47 import com.android.launcher3.util.Executors;
48 import com.android.launcher3.util.Themes;
49 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
50 
51 /**
52  * {@inheritDoc}
53  */
54 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
55         implements TouchCompleteListener, View.OnLongClickListener {
56 
57     // Related to the auto-advancing of widgets
58     private static final long ADVANCE_INTERVAL = 20000;
59     private static final long ADVANCE_STAGGER = 250;
60 
61     // Maintains a list of widget ids which are supposed to be auto advanced.
62     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
63 
64     protected final LayoutInflater mInflater;
65 
66     private final CheckLongPressHelper mLongPressHelper;
67     private final StylusEventHelper mStylusEventHelper;
68     protected final Launcher mLauncher;
69 
70     @ViewDebug.ExportedProperty(category = "launcher")
71     private boolean mReinflateOnConfigChange;
72 
73     private float mSlop;
74 
75     private boolean mIsScrollable;
76     private boolean mIsAttachedToWindow;
77     private boolean mIsAutoAdvanceRegistered;
78     private Runnable mAutoAdvanceRunnable;
79 
80     /**
81      * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
82      */
83     private float mScaleToFit = 1f;
84 
85     /**
86      * The translation values to center the widget within its cellspans.
87      */
88     private final PointF mTranslationForCentering = new PointF(0, 0);
89 
LauncherAppWidgetHostView(Context context)90     public LauncherAppWidgetHostView(Context context) {
91         super(context);
92         mLauncher = Launcher.getLauncher(context);
93         mLongPressHelper = new CheckLongPressHelper(this, this);
94         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
95         mInflater = LayoutInflater.from(context);
96         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
97         setBackgroundResource(R.drawable.widget_internal_focus_bg);
98 
99         if (Utilities.ATLEAST_OREO) {
100             setExecutor(Executors.THREAD_POOL_EXECUTOR);
101         }
102         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
103             setOnLightBackground(true);
104         }
105     }
106 
107     @Override
onLongClick(View view)108     public boolean onLongClick(View view) {
109         if (mIsScrollable) {
110             DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
111             dragLayer.requestDisallowInterceptTouchEvent(false);
112         }
113         view.performLongClick();
114         return true;
115     }
116 
117     @Override
getErrorView()118     protected View getErrorView() {
119         return mInflater.inflate(R.layout.appwidget_error, this, false);
120     }
121 
122     @Override
updateAppWidget(RemoteViews remoteViews)123     public void updateAppWidget(RemoteViews remoteViews) {
124         super.updateAppWidget(remoteViews);
125 
126         // The provider info or the views might have changed.
127         checkIfAutoAdvance();
128 
129         // It is possible that widgets can receive updates while launcher is not in the foreground.
130         // Consequently, the widgets will be inflated for the orientation of the foreground activity
131         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
132         // orientation.
133         mReinflateOnConfigChange = !isSameOrientation();
134     }
135 
isSameOrientation()136     private boolean isSameOrientation() {
137         return mLauncher.getResources().getConfiguration().orientation ==
138                 mLauncher.getOrientation();
139     }
140 
checkScrollableRecursively(ViewGroup viewGroup)141     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
142         if (viewGroup instanceof AdapterView) {
143             return true;
144         } else {
145             for (int i=0; i < viewGroup.getChildCount(); i++) {
146                 View child = viewGroup.getChildAt(i);
147                 if (child instanceof ViewGroup) {
148                     if (checkScrollableRecursively((ViewGroup) child)) {
149                         return true;
150                     }
151                 }
152             }
153         }
154         return false;
155     }
156 
onInterceptTouchEvent(MotionEvent ev)157     public boolean onInterceptTouchEvent(MotionEvent ev) {
158         // Just in case the previous long press hasn't been cleared, we make sure to start fresh
159         // on touch down.
160         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
161             mLongPressHelper.cancelLongPress();
162         }
163 
164         // Consume any touch events for ourselves after longpress is triggered
165         if (mLongPressHelper.hasPerformedLongPress()) {
166             mLongPressHelper.cancelLongPress();
167             return true;
168         }
169 
170         // Watch for longpress or stylus button press events at this level to
171         // make sure users can always pick up this widget
172         if (mStylusEventHelper.onMotionEvent(ev)) {
173             mLongPressHelper.cancelLongPress();
174             return true;
175         }
176 
177         switch (ev.getAction()) {
178             case MotionEvent.ACTION_DOWN: {
179                 DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
180 
181                 if (mIsScrollable) {
182                      dragLayer.requestDisallowInterceptTouchEvent(true);
183                 }
184                 if (!mStylusEventHelper.inStylusButtonPressed()) {
185                     mLongPressHelper.postCheckForLongPress();
186                 }
187                 dragLayer.setTouchCompleteListener(this);
188                 break;
189             }
190 
191             case MotionEvent.ACTION_UP:
192             case MotionEvent.ACTION_CANCEL:
193                 mLongPressHelper.cancelLongPress();
194                 break;
195             case MotionEvent.ACTION_MOVE:
196                 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
197                     mLongPressHelper.cancelLongPress();
198                 }
199                 break;
200         }
201 
202         // Otherwise continue letting touch events fall through to children
203         return false;
204     }
205 
onTouchEvent(MotionEvent ev)206     public boolean onTouchEvent(MotionEvent ev) {
207         // If the widget does not handle touch, then cancel
208         // long press when we release the touch
209         switch (ev.getAction()) {
210             case MotionEvent.ACTION_UP:
211             case MotionEvent.ACTION_CANCEL:
212                 mLongPressHelper.cancelLongPress();
213                 break;
214             case MotionEvent.ACTION_MOVE:
215                 if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
216                     mLongPressHelper.cancelLongPress();
217                 }
218                 break;
219         }
220         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
221         return true;
222     }
223 
224     @Override
onAttachedToWindow()225     protected void onAttachedToWindow() {
226         super.onAttachedToWindow();
227         mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
228 
229         mIsAttachedToWindow = true;
230         checkIfAutoAdvance();
231     }
232 
233     @Override
onDetachedFromWindow()234     protected void onDetachedFromWindow() {
235         super.onDetachedFromWindow();
236 
237         // We can't directly use isAttachedToWindow() here, as this is called before the internal
238         // state is updated. So isAttachedToWindow() will return true until next frame.
239         mIsAttachedToWindow = false;
240         checkIfAutoAdvance();
241     }
242 
243     @Override
cancelLongPress()244     public void cancelLongPress() {
245         super.cancelLongPress();
246         mLongPressHelper.cancelLongPress();
247     }
248 
249     @Override
getAppWidgetInfo()250     public AppWidgetProviderInfo getAppWidgetInfo() {
251         AppWidgetProviderInfo info = super.getAppWidgetInfo();
252         if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
253             throw new IllegalStateException("Launcher widget must have"
254                     + " LauncherAppWidgetProviderInfo");
255         }
256         return info;
257     }
258 
259     @Override
onTouchComplete()260     public void onTouchComplete() {
261         if (!mLongPressHelper.hasPerformedLongPress()) {
262             // If a long press has been performed, we don't want to clear the record of that since
263             // we still may be receiving a touch up which we want to intercept
264             mLongPressHelper.cancelLongPress();
265         }
266     }
267 
switchToErrorView()268     public void switchToErrorView() {
269         // Update the widget with 0 Layout id, to reset the view to error view.
270         updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
271     }
272 
273     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)274     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
275         try {
276             super.onLayout(changed, left, top, right, bottom);
277         } catch (final RuntimeException e) {
278             post(new Runnable() {
279                 @Override
280                 public void run() {
281                     switchToErrorView();
282                 }
283             });
284         }
285 
286         mIsScrollable = checkScrollableRecursively(this);
287     }
288 
289     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)290     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
291         super.onInitializeAccessibilityNodeInfo(info);
292         info.setClassName(getClass().getName());
293     }
294 
295     @Override
onWindowVisibilityChanged(int visibility)296     protected void onWindowVisibilityChanged(int visibility) {
297         super.onWindowVisibilityChanged(visibility);
298         maybeRegisterAutoAdvance();
299     }
300 
checkIfAutoAdvance()301     private void checkIfAutoAdvance() {
302         boolean isAutoAdvance = false;
303         Advanceable target = getAdvanceable();
304         if (target != null) {
305             isAutoAdvance = true;
306             target.fyiWillBeAdvancedByHostKThx();
307         }
308 
309         boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
310         if (isAutoAdvance != wasAutoAdvance) {
311             if (isAutoAdvance) {
312                 sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
313             } else {
314                 sAutoAdvanceWidgetIds.delete(getAppWidgetId());
315             }
316             maybeRegisterAutoAdvance();
317         }
318     }
319 
getAdvanceable()320     private Advanceable getAdvanceable() {
321         AppWidgetProviderInfo info = getAppWidgetInfo();
322         if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
323             return null;
324         }
325         View v = findViewById(info.autoAdvanceViewId);
326         return (v instanceof Advanceable) ? (Advanceable) v : null;
327     }
328 
maybeRegisterAutoAdvance()329     private void maybeRegisterAutoAdvance() {
330         Handler handler = getHandler();
331         boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
332                 && (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
333         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
334             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
335             if (mAutoAdvanceRunnable == null) {
336                 mAutoAdvanceRunnable = this::runAutoAdvance;
337             }
338 
339             handler.removeCallbacks(mAutoAdvanceRunnable);
340             scheduleNextAdvance();
341         }
342     }
343 
scheduleNextAdvance()344     private void scheduleNextAdvance() {
345         if (!mIsAutoAdvanceRegistered) {
346             return;
347         }
348         long now = SystemClock.uptimeMillis();
349         long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
350                 ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
351         Handler handler = getHandler();
352         if (handler != null) {
353             handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
354         }
355     }
356 
runAutoAdvance()357     private void runAutoAdvance() {
358         Advanceable target = getAdvanceable();
359         if (target != null) {
360             target.advance();
361         }
362         scheduleNextAdvance();
363     }
364 
setScaleToFit(float scale)365     public void setScaleToFit(float scale) {
366         mScaleToFit = scale;
367         setScaleX(scale);
368         setScaleY(scale);
369     }
370 
getScaleToFit()371     public float getScaleToFit() {
372         return mScaleToFit;
373     }
374 
setTranslationForCentering(float x, float y)375     public void setTranslationForCentering(float x, float y) {
376         mTranslationForCentering.set(x, y);
377         setTranslationX(x);
378         setTranslationY(y);
379     }
380 
getTranslationForCentering()381     public PointF getTranslationForCentering() {
382         return mTranslationForCentering;
383     }
384 
385     @Override
onConfigurationChanged(Configuration newConfig)386     protected void onConfigurationChanged(Configuration newConfig) {
387         super.onConfigurationChanged(newConfig);
388 
389         // Only reinflate when the final configuration is same as the required configuration
390         if (mReinflateOnConfigChange && isSameOrientation()) {
391             mReinflateOnConfigChange = false;
392             reInflate();
393         }
394     }
395 
reInflate()396     public void reInflate() {
397         if (!isAttachedToWindow()) {
398             return;
399         }
400         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
401         // Remove and rebind the current widget (which was inflated in the wrong
402         // orientation), but don't delete it from the database
403         mLauncher.removeItem(this, info, false  /* deleteFromDb */);
404         mLauncher.bindAppWidget(info);
405     }
406 
407     @Override
shouldAllowDirectClick()408     protected boolean shouldAllowDirectClick() {
409         if (getTag() instanceof ItemInfo) {
410             ItemInfo item = (ItemInfo) getTag();
411             return item.spanX == 1 && item.spanY == 1;
412         }
413         return false;
414     }
415 }
416