LauncherAppWidgetHostView.java revision c11fac355d5057ad066adc3abbbb2b2d64fba5e7
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
17package com.android.launcher3.widget;
18
19import android.appwidget.AppWidgetHostView;
20import android.appwidget.AppWidgetProviderInfo;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.graphics.PointF;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.SystemClock;
27import android.util.SparseBooleanArray;
28import android.view.KeyEvent;
29import android.view.LayoutInflater;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewDebug;
34import android.view.ViewGroup;
35import android.view.accessibility.AccessibilityNodeInfo;
36import android.widget.AdapterView;
37import android.widget.Advanceable;
38import android.widget.RemoteViews;
39
40import com.android.launcher3.CheckLongPressHelper;
41import com.android.launcher3.ItemInfo;
42import com.android.launcher3.Launcher;
43import com.android.launcher3.LauncherAppWidgetInfo;
44import com.android.launcher3.LauncherAppWidgetProviderInfo;
45import com.android.launcher3.R;
46import com.android.launcher3.SimpleOnStylusPressListener;
47import com.android.launcher3.StylusEventHelper;
48import com.android.launcher3.Utilities;
49import com.android.launcher3.dragndrop.DragLayer;
50import com.android.launcher3.dragndrop.DragLayer.TouchCompleteListener;
51
52import java.util.ArrayList;
53
54/**
55 * {@inheritDoc}
56 */
57public 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
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
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
121    protected View getErrorView() {
122        return mInflater.inflate(R.layout.appwidget_error, this, false);
123    }
124
125    @Override
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
139    private boolean isSameOrientation() {
140        return mLauncher.getResources().getConfiguration().orientation ==
141                mLauncher.getOrientation();
142    }
143
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
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
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
227    protected void onAttachedToWindow() {
228        super.onAttachedToWindow();
229        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
230
231        mIsAttachedToWindow = true;
232        checkIfAutoAdvance();
233    }
234
235    @Override
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
246    public void cancelLongPress() {
247        super.cancelLongPress();
248        mLongPressHelper.cancelLongPress();
249    }
250
251    @Override
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
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
271    public int getDescendantFocusability() {
272        return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
273                : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
274    }
275
276    @Override
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
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
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
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
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
347    public void clearChildFocus(View child) {
348        super.clearChildFocus(child);
349        dispatchChildFocus(false);
350    }
351
352    @Override
353    public boolean dispatchUnhandledMove(View focused, int direction) {
354        return mChildrenFocused;
355    }
356
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
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
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
384    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
385        super.onInitializeAccessibilityNodeInfo(info);
386        info.setClassName(getClass().getName());
387    }
388
389    @Override
390    protected void onWindowVisibilityChanged(int visibility) {
391        super.onWindowVisibilityChanged(visibility);
392        maybeRegisterAutoAdvance();
393    }
394
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
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
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
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
456    private void runAutoAdvance() {
457        Advanceable target = getAdvanceable();
458        if (target != null) {
459            target.advance();
460        }
461        scheduleNextAdvance();
462    }
463
464    public void setScaleToFit(float scale) {
465        mScaleToFit = scale;
466        setScaleX(scale);
467        setScaleY(scale);
468    }
469
470    public float getScaleToFit() {
471        return mScaleToFit;
472    }
473
474    public void setTranslationForCentering(float x, float y) {
475        mTranslationForCentering.set(x, y);
476        setTranslationX(x);
477        setTranslationY(y);
478    }
479
480    public PointF getTranslationForCentering() {
481        return mTranslationForCentering;
482    }
483
484    @Override
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
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