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