1 /*
2  * Copyright (C) 2018 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.graphics.Rect;
23 import android.view.KeyEvent;
24 import android.view.View;
25 import android.view.ViewDebug;
26 import android.view.ViewGroup;
27 
28 import com.android.launcher3.Reorderable;
29 import com.android.launcher3.dragndrop.DraggableView;
30 import com.android.launcher3.util.MultiTranslateDelegate;
31 import com.android.launcher3.views.ActivityContext;
32 
33 import java.util.ArrayList;
34 
35 /**
36  * Extension of AppWidgetHostView with support for controlled keyboard navigation.
37  */
38 public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
39         implements DraggableView, Reorderable {
40 
41     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
42 
43     /**
44      * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
45      */
46     private float mScaleToFit = 1f;
47 
48     private float mScaleForReorderBounce = 1f;
49 
50     @ViewDebug.ExportedProperty(category = "launcher")
51     private boolean mChildrenFocused;
52 
53     protected final ActivityContext mActivity;
54 
55     private boolean mDisableSetPadding = false;
56 
NavigableAppWidgetHostView(Context context)57     public NavigableAppWidgetHostView(Context context) {
58         super(context);
59         mActivity = ActivityContext.lookupContext(context);
60     }
61 
62     @Override
getDescendantFocusability()63     public int getDescendantFocusability() {
64         return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
65                 : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
66     }
67 
68     @Override
dispatchKeyEvent(KeyEvent event)69     public boolean dispatchKeyEvent(KeyEvent event) {
70         if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE
71                 && event.getAction() == KeyEvent.ACTION_UP) {
72             mChildrenFocused = false;
73             requestFocus();
74             return true;
75         }
76         return super.dispatchKeyEvent(event);
77     }
78 
79     @Override
onKeyDown(int keyCode, KeyEvent event)80     public boolean onKeyDown(int keyCode, KeyEvent event) {
81         if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
82             event.startTracking();
83             return true;
84         }
85         return super.onKeyDown(keyCode, event);
86     }
87 
88     @Override
onKeyUp(int keyCode, KeyEvent event)89     public boolean onKeyUp(int keyCode, KeyEvent event) {
90         if (event.isTracking()) {
91             if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) {
92                 mChildrenFocused = true;
93                 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD);
94                 focusableChildren.remove(this);
95                 int childrenCount = focusableChildren.size();
96                 switch (childrenCount) {
97                     case 0:
98                         mChildrenFocused = false;
99                         break;
100                     case 1: {
101                         if (shouldAllowDirectClick()) {
102                             focusableChildren.get(0).performClick();
103                             mChildrenFocused = false;
104                             return true;
105                         }
106                         // continue;
107                     }
108                     default:
109                         focusableChildren.get(0).requestFocus();
110                         return true;
111                 }
112             }
113         }
114         return super.onKeyUp(keyCode, event);
115     }
116 
117     /**
118      * For a widget with only a single interactive element, return true if whole widget should act
119      * as a single interactive element, and clicking 'enter' should activate the child element
120      * directly. Otherwise clicking 'enter' will only move the focus inside the widget.
121      */
shouldAllowDirectClick()122     protected abstract boolean shouldAllowDirectClick();
123 
124     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)125     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
126         if (gainFocus) {
127             mChildrenFocused = false;
128             dispatchChildFocus(false);
129         }
130         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
131     }
132 
133     @Override
requestChildFocus(View child, View focused)134     public void requestChildFocus(View child, View focused) {
135         super.requestChildFocus(child, focused);
136         dispatchChildFocus(mChildrenFocused && focused != null);
137         if (focused != null) {
138             focused.setFocusableInTouchMode(false);
139         }
140     }
141 
142     @Override
clearChildFocus(View child)143     public void clearChildFocus(View child) {
144         super.clearChildFocus(child);
145         dispatchChildFocus(false);
146     }
147 
148     @Override
setAppWidget(int appWidgetId, AppWidgetProviderInfo info)149     public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
150         // Prevent default padding being set on the view based on provider info. Launcher manages
151         // its own widget spacing
152         mDisableSetPadding = true;
153         super.setAppWidget(appWidgetId, info);
154         mDisableSetPadding = false;
155     }
156 
157     @Override
setPadding(int left, int top, int right, int bottom)158     public void setPadding(int left, int top, int right, int bottom) {
159         if (!mDisableSetPadding) {
160             super.setPadding(left, top, right, bottom);
161         }
162     }
163 
164     @Override
dispatchUnhandledMove(View focused, int direction)165     public boolean dispatchUnhandledMove(View focused, int direction) {
166         return mChildrenFocused;
167     }
168 
dispatchChildFocus(boolean childIsFocused)169     private void dispatchChildFocus(boolean childIsFocused) {
170         // The host view's background changes when selected, to indicate the focus is inside.
171         setSelected(childIsFocused);
172     }
173 
updateScale()174     private void updateScale() {
175         super.setScaleX(mScaleToFit * mScaleForReorderBounce);
176         super.setScaleY(mScaleToFit * mScaleForReorderBounce);
177     }
178 
179     @Override
getTranslateDelegate()180     public MultiTranslateDelegate getTranslateDelegate() {
181         return mTranslateDelegate;
182     }
183 
184     @Override
setReorderBounceScale(float scale)185     public void setReorderBounceScale(float scale) {
186         mScaleForReorderBounce = scale;
187         updateScale();
188     }
189 
190     @Override
getReorderBounceScale()191     public float getReorderBounceScale() {
192         return mScaleForReorderBounce;
193     }
194 
setScaleToFit(float scale)195     public void setScaleToFit(float scale) {
196         mScaleToFit = scale;
197         updateScale();
198     }
199 
getScaleToFit()200     public float getScaleToFit() {
201         return mScaleToFit;
202     }
203 
204     @Override
getViewType()205     public int getViewType() {
206         return DRAGGABLE_WIDGET;
207     }
208 
209     @Override
getWorkspaceVisualDragBounds(Rect bounds)210     public void getWorkspaceVisualDragBounds(Rect bounds) {
211         int width = (int) (getMeasuredWidth() * mScaleToFit);
212         int height = (int) (getMeasuredHeight() * mScaleToFit);
213         bounds.set(0, 0, width, height);
214     }
215 }
216