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