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;
18
19import android.appwidget.AppWidgetHostView;
20import android.appwidget.AppWidgetProviderInfo;
21import android.content.Context;
22import android.graphics.Rect;
23import android.view.KeyEvent;
24import android.view.LayoutInflater;
25import android.view.MotionEvent;
26import android.view.View;
27import android.view.ViewConfiguration;
28import android.view.ViewDebug;
29import android.view.ViewGroup;
30import android.view.accessibility.AccessibilityNodeInfo;
31import android.widget.RemoteViews;
32
33import com.android.launcher3.dragndrop.DragLayer.TouchCompleteListener;
34
35import java.util.ArrayList;
36
37/**
38 * {@inheritDoc}
39 */
40public class LauncherAppWidgetHostView extends AppWidgetHostView implements TouchCompleteListener {
41
42    LayoutInflater mInflater;
43
44    private CheckLongPressHelper mLongPressHelper;
45    private StylusEventHelper mStylusEventHelper;
46    private Context mContext;
47    @ViewDebug.ExportedProperty(category = "launcher")
48    private int mPreviousOrientation;
49
50    private float mSlop;
51
52    @ViewDebug.ExportedProperty(category = "launcher")
53    private boolean mChildrenFocused;
54
55    protected int mErrorViewId = R.layout.appwidget_error;
56
57    public LauncherAppWidgetHostView(Context context) {
58        super(context);
59        mContext = context;
60        mLongPressHelper = new CheckLongPressHelper(this);
61        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
62        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
63        setAccessibilityDelegate(Launcher.getLauncher(context).getAccessibilityDelegate());
64        setBackgroundResource(R.drawable.widget_internal_focus_bg);
65    }
66
67    @Override
68    protected View getErrorView() {
69        return mInflater.inflate(mErrorViewId, this, false);
70    }
71
72    public void updateLastInflationOrientation() {
73        mPreviousOrientation = mContext.getResources().getConfiguration().orientation;
74    }
75
76    @Override
77    public void updateAppWidget(RemoteViews remoteViews) {
78        // Store the orientation in which the widget was inflated
79        updateLastInflationOrientation();
80        super.updateAppWidget(remoteViews);
81    }
82
83    public boolean isReinflateRequired() {
84        // Re-inflate is required if the orientation has changed since last inflated.
85        int orientation = mContext.getResources().getConfiguration().orientation;
86        if (mPreviousOrientation != orientation) {
87           return true;
88       }
89       return false;
90    }
91
92    public boolean onInterceptTouchEvent(MotionEvent ev) {
93        // Just in case the previous long press hasn't been cleared, we make sure to start fresh
94        // on touch down.
95        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
96            mLongPressHelper.cancelLongPress();
97        }
98
99        // Consume any touch events for ourselves after longpress is triggered
100        if (mLongPressHelper.hasPerformedLongPress()) {
101            mLongPressHelper.cancelLongPress();
102            return true;
103        }
104
105        // Watch for longpress or stylus button press events at this level to
106        // make sure users can always pick up this widget
107        if (mStylusEventHelper.onMotionEvent(ev)) {
108            mLongPressHelper.cancelLongPress();
109            return true;
110        }
111        switch (ev.getAction()) {
112            case MotionEvent.ACTION_DOWN: {
113                if (!mStylusEventHelper.inStylusButtonPressed()) {
114                    mLongPressHelper.postCheckForLongPress();
115                }
116                Launcher.getLauncher(getContext()).getDragLayer().setTouchCompleteListener(this);
117                break;
118            }
119
120            case MotionEvent.ACTION_UP:
121            case MotionEvent.ACTION_CANCEL:
122                mLongPressHelper.cancelLongPress();
123                break;
124            case MotionEvent.ACTION_MOVE:
125                if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
126                    mLongPressHelper.cancelLongPress();
127                }
128                break;
129        }
130
131        // Otherwise continue letting touch events fall through to children
132        return false;
133    }
134
135    public boolean onTouchEvent(MotionEvent ev) {
136        // If the widget does not handle touch, then cancel
137        // long press when we release the touch
138        switch (ev.getAction()) {
139            case MotionEvent.ACTION_UP:
140            case MotionEvent.ACTION_CANCEL:
141                mLongPressHelper.cancelLongPress();
142                break;
143            case MotionEvent.ACTION_MOVE:
144                if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
145                    mLongPressHelper.cancelLongPress();
146                }
147                break;
148        }
149        return false;
150    }
151
152    @Override
153    protected void onAttachedToWindow() {
154        super.onAttachedToWindow();
155        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
156    }
157
158    @Override
159    public void cancelLongPress() {
160        super.cancelLongPress();
161        mLongPressHelper.cancelLongPress();
162    }
163
164    @Override
165    public AppWidgetProviderInfo getAppWidgetInfo() {
166        AppWidgetProviderInfo info = super.getAppWidgetInfo();
167        if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
168            throw new IllegalStateException("Launcher widget must have"
169                    + " LauncherAppWidgetProviderInfo");
170        }
171        return info;
172    }
173
174    public LauncherAppWidgetProviderInfo getLauncherAppWidgetProviderInfo() {
175        return (LauncherAppWidgetProviderInfo) getAppWidgetInfo();
176    }
177
178    @Override
179    public void onTouchComplete() {
180        if (!mLongPressHelper.hasPerformedLongPress()) {
181            // If a long press has been performed, we don't want to clear the record of that since
182            // we still may be receiving a touch up which we want to intercept
183            mLongPressHelper.cancelLongPress();
184        }
185    }
186
187    @Override
188    public int getDescendantFocusability() {
189        return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
190                : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
191    }
192
193    @Override
194    public boolean dispatchKeyEvent(KeyEvent event) {
195        if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE
196                && event.getAction() == KeyEvent.ACTION_UP) {
197            mChildrenFocused = false;
198            requestFocus();
199            return true;
200        }
201        return super.dispatchKeyEvent(event);
202    }
203
204    @Override
205    public boolean onKeyDown(int keyCode, KeyEvent event) {
206        if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
207            event.startTracking();
208            return true;
209        }
210        return super.onKeyDown(keyCode, event);
211    }
212
213    @Override
214    public boolean onKeyUp(int keyCode, KeyEvent event) {
215        if (event.isTracking()) {
216            if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
217                mChildrenFocused = true;
218                ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD);
219                focusableChildren.remove(this);
220                int childrenCount = focusableChildren.size();
221                switch (childrenCount) {
222                    case 0:
223                        mChildrenFocused = false;
224                        break;
225                    case 1: {
226                        if (getTag() instanceof ItemInfo) {
227                            ItemInfo item = (ItemInfo) getTag();
228                            if (item.spanX == 1 && item.spanY == 1) {
229                                focusableChildren.get(0).performClick();
230                                mChildrenFocused = false;
231                                return true;
232                            }
233                        }
234                        // continue;
235                    }
236                    default:
237                        focusableChildren.get(0).requestFocus();
238                        return true;
239                }
240            }
241        }
242        return super.onKeyUp(keyCode, event);
243    }
244
245    @Override
246    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
247        if (gainFocus) {
248            mChildrenFocused = false;
249            dispatchChildFocus(false);
250        }
251        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
252    }
253
254    @Override
255    public void requestChildFocus(View child, View focused) {
256        super.requestChildFocus(child, focused);
257        dispatchChildFocus(mChildrenFocused && focused != null);
258        if (focused != null) {
259            focused.setFocusableInTouchMode(false);
260        }
261    }
262
263    @Override
264    public void clearChildFocus(View child) {
265        super.clearChildFocus(child);
266        dispatchChildFocus(false);
267    }
268
269    @Override
270    public boolean dispatchUnhandledMove(View focused, int direction) {
271        return mChildrenFocused;
272    }
273
274    private void dispatchChildFocus(boolean childIsFocused) {
275        // The host view's background changes when selected, to indicate the focus is inside.
276        setSelected(childIsFocused);
277    }
278
279    @Override
280    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
281        try {
282            super.onLayout(changed, left, top, right, bottom);
283        } catch (final RuntimeException e) {
284            post(new Runnable() {
285                @Override
286                public void run() {
287                    // Update the widget with 0 Layout id, to reset the view to error view.
288                    updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
289                }
290            });
291        }
292    }
293
294    @Override
295    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
296        super.onInitializeAccessibilityNodeInfo(info);
297        info.setClassName(getClass().getName());
298    }
299}
300