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