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