1 /*
2  * Copyright (C) 2008 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.systemui.statusbar.policy;
18 
19 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
20 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
21 
22 import android.app.ActivityManager;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.TypedArray;
26 import android.graphics.drawable.Drawable;
27 import android.graphics.drawable.Icon;
28 import android.hardware.input.InputManager;
29 import android.media.AudioManager;
30 import android.metrics.LogMaker;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.os.SystemClock;
34 import android.util.AttributeSet;
35 import android.util.TypedValue;
36 import android.view.HapticFeedbackConstants;
37 import android.view.InputDevice;
38 import android.view.KeyCharacterMap;
39 import android.view.KeyEvent;
40 import android.view.MotionEvent;
41 import android.view.SoundEffectConstants;
42 import android.view.View;
43 import android.view.ViewConfiguration;
44 import android.view.accessibility.AccessibilityEvent;
45 import android.view.accessibility.AccessibilityNodeInfo;
46 import android.widget.ImageView;
47 import com.android.internal.logging.MetricsLogger;
48 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
49 import com.android.systemui.Dependency;
50 import com.android.systemui.OverviewProxyService;
51 import com.android.systemui.R;
52 import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
53 import com.android.systemui.shared.system.NavigationBarCompat;
54 
55 public class KeyButtonView extends ImageView implements ButtonInterface {
56     private static final String TAG = KeyButtonView.class.getSimpleName();
57 
58     private final boolean mPlaySounds;
59     private int mContentDescriptionRes;
60     private long mDownTime;
61     private int mCode;
62     private int mTouchDownX;
63     private int mTouchDownY;
64     private boolean mIsVertical;
65     private boolean mSupportsLongpress = true;
66     private AudioManager mAudioManager;
67     private boolean mGestureAborted;
68     private boolean mLongClicked;
69     private OnClickListener mOnClickListener;
70     private final KeyButtonRipple mRipple;
71     private final OverviewProxyService mOverviewProxyService;
72     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
73 
74     private final Runnable mCheckLongPress = new Runnable() {
75         public void run() {
76             if (isPressed()) {
77                 // Log.d("KeyButtonView", "longpressed: " + this);
78                 if (isLongClickable()) {
79                     // Just an old-fashioned ImageView
80                     performLongClick();
81                     mLongClicked = true;
82                 } else if (mSupportsLongpress) {
83                     sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
84                     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
85                     mLongClicked = true;
86                 }
87             }
88         }
89     };
90 
KeyButtonView(Context context, AttributeSet attrs)91     public KeyButtonView(Context context, AttributeSet attrs) {
92         this(context, attrs, 0);
93     }
94 
KeyButtonView(Context context, AttributeSet attrs, int defStyle)95     public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
96         super(context, attrs);
97 
98         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
99                 defStyle, 0);
100 
101         mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
102 
103         mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
104         mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);
105 
106         TypedValue value = new TypedValue();
107         if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
108             mContentDescriptionRes = value.resourceId;
109         }
110 
111         a.recycle();
112 
113         setClickable(true);
114         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
115 
116         mRipple = new KeyButtonRipple(context, this);
117         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
118         setBackground(mRipple);
119         forceHasOverlappingRendering(false);
120     }
121 
122     @Override
isClickable()123     public boolean isClickable() {
124         return mCode != 0 || super.isClickable();
125     }
126 
setCode(int code)127     public void setCode(int code) {
128         mCode = code;
129     }
130 
131     @Override
setOnClickListener(OnClickListener onClickListener)132     public void setOnClickListener(OnClickListener onClickListener) {
133         super.setOnClickListener(onClickListener);
134         mOnClickListener = onClickListener;
135     }
136 
loadAsync(Icon icon)137     public void loadAsync(Icon icon) {
138         new AsyncTask<Icon, Void, Drawable>() {
139             @Override
140             protected Drawable doInBackground(Icon... params) {
141                 return params[0].loadDrawable(mContext);
142             }
143 
144             @Override
145             protected void onPostExecute(Drawable drawable) {
146                 setImageDrawable(drawable);
147             }
148         }.execute(icon);
149     }
150 
151     @Override
onConfigurationChanged(Configuration newConfig)152     protected void onConfigurationChanged(Configuration newConfig) {
153         super.onConfigurationChanged(newConfig);
154 
155         if (mContentDescriptionRes != 0) {
156             setContentDescription(mContext.getString(mContentDescriptionRes));
157         }
158     }
159 
160     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)161     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
162         super.onInitializeAccessibilityNodeInfo(info);
163         if (mCode != 0) {
164             info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
165             if (mSupportsLongpress || isLongClickable()) {
166                 info.addAction(
167                         new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
168             }
169         }
170     }
171 
172     @Override
onWindowVisibilityChanged(int visibility)173     protected void onWindowVisibilityChanged(int visibility) {
174         super.onWindowVisibilityChanged(visibility);
175         if (visibility != View.VISIBLE) {
176             jumpDrawablesToCurrentState();
177         }
178     }
179 
180     @Override
performAccessibilityActionInternal(int action, Bundle arguments)181     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
182         if (action == ACTION_CLICK && mCode != 0) {
183             sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
184             sendEvent(KeyEvent.ACTION_UP, 0);
185             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
186             playSoundEffect(SoundEffectConstants.CLICK);
187             return true;
188         } else if (action == ACTION_LONG_CLICK && mCode != 0) {
189             sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
190             sendEvent(KeyEvent.ACTION_UP, 0);
191             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
192             return true;
193         }
194         return super.performAccessibilityActionInternal(action, arguments);
195     }
196 
onTouchEvent(MotionEvent ev)197     public boolean onTouchEvent(MotionEvent ev) {
198         final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI();
199         final int action = ev.getAction();
200         int x, y;
201         if (action == MotionEvent.ACTION_DOWN) {
202             mGestureAborted = false;
203         }
204         if (mGestureAborted) {
205             setPressed(false);
206             return false;
207         }
208 
209         switch (action) {
210             case MotionEvent.ACTION_DOWN:
211                 mDownTime = SystemClock.uptimeMillis();
212                 mLongClicked = false;
213                 setPressed(true);
214 
215                 // Use raw X and Y to detect gestures in case a parent changes the x and y values
216                 mTouchDownX = (int) ev.getRawX();
217                 mTouchDownY = (int) ev.getRawY();
218                 if (mCode != 0) {
219                     sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
220                 } else {
221                     // Provide the same haptic feedback that the system offers for virtual keys.
222                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
223                 }
224                 if (!showSwipeUI) {
225                     playSoundEffect(SoundEffectConstants.CLICK);
226                 }
227                 removeCallbacks(mCheckLongPress);
228                 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
229                 break;
230             case MotionEvent.ACTION_MOVE:
231                 x = (int)ev.getRawX();
232                 y = (int)ev.getRawY();
233 
234                 boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) > (mIsVertical
235                         ? NavigationBarCompat.getQuickScrubTouchSlopPx()
236                         : NavigationBarCompat.getQuickStepTouchSlopPx());
237                 boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) > (mIsVertical
238                         ? NavigationBarCompat.getQuickStepTouchSlopPx()
239                         : NavigationBarCompat.getQuickScrubTouchSlopPx());
240                 if (exceededTouchSlopX || exceededTouchSlopY) {
241                     // When quick step is enabled, prevent animating the ripple triggered by
242                     // setPressed and decide to run it on touch up
243                     setPressed(false);
244                     removeCallbacks(mCheckLongPress);
245                 }
246                 break;
247             case MotionEvent.ACTION_CANCEL:
248                 setPressed(false);
249                 if (mCode != 0) {
250                     sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
251                 }
252                 removeCallbacks(mCheckLongPress);
253                 break;
254             case MotionEvent.ACTION_UP:
255                 final boolean doIt = isPressed() && !mLongClicked;
256                 setPressed(false);
257                 final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
258                 if (showSwipeUI) {
259                     if (doIt) {
260                         // Apply haptic feedback on touch up since there is none on touch down
261                         performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
262                         playSoundEffect(SoundEffectConstants.CLICK);
263                     }
264                 } else if (doHapticFeedback && !mLongClicked) {
265                     // Always send a release ourselves because it doesn't seem to be sent elsewhere
266                     // and it feels weird to sometimes get a release haptic and other times not.
267                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
268                 }
269                 if (mCode != 0) {
270                     if (doIt) {
271                         sendEvent(KeyEvent.ACTION_UP, 0);
272                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
273                     } else {
274                         sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
275                     }
276                 } else {
277                     // no key code, just a regular ImageView
278                     if (doIt && mOnClickListener != null) {
279                         mOnClickListener.onClick(this);
280                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
281                     }
282                 }
283                 removeCallbacks(mCheckLongPress);
284                 break;
285         }
286 
287         return true;
288     }
289 
playSoundEffect(int soundConstant)290     public void playSoundEffect(int soundConstant) {
291         if (!mPlaySounds) return;
292         mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser());
293     }
294 
sendEvent(int action, int flags)295     public void sendEvent(int action, int flags) {
296         sendEvent(action, flags, SystemClock.uptimeMillis());
297     }
298 
sendEvent(int action, int flags, long when)299     void sendEvent(int action, int flags, long when) {
300         mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
301                 .setType(MetricsEvent.TYPE_ACTION)
302                 .setSubtype(mCode)
303                 .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
304                 .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
305         final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
306         final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
307                 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
308                 flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
309                 InputDevice.SOURCE_KEYBOARD);
310         InputManager.getInstance().injectInputEvent(ev,
311                 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
312     }
313 
314     @Override
abortCurrentGesture()315     public void abortCurrentGesture() {
316         setPressed(false);
317         mRipple.abortDelayedRipple();
318         mGestureAborted = true;
319     }
320 
321     @Override
setDarkIntensity(float darkIntensity)322     public void setDarkIntensity(float darkIntensity) {
323         Drawable drawable = getDrawable();
324         if (drawable != null) {
325             ((KeyButtonDrawable) getDrawable()).setDarkIntensity(darkIntensity);
326 
327             // Since we reuse the same drawable for multiple views, we need to invalidate the view
328             // manually.
329             invalidate();
330         }
331         mRipple.setDarkIntensity(darkIntensity);
332     }
333 
334     @Override
setDelayTouchFeedback(boolean shouldDelay)335     public void setDelayTouchFeedback(boolean shouldDelay) {
336         mRipple.setDelayTouchFeedback(shouldDelay);
337     }
338 
339     @Override
setVertical(boolean vertical)340     public void setVertical(boolean vertical) {
341         mIsVertical = vertical;
342     }
343 }
344 
345 
346