1 /*
2  * Copyright (C) 2020 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.navigationbar.buttons;
18 
19 import static android.view.Display.INVALID_DISPLAY;
20 import static android.view.KeyEvent.KEYCODE_UNKNOWN;
21 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
22 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
23 
24 import android.app.ActivityManager;
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.Paint;
30 import android.graphics.drawable.Drawable;
31 import android.graphics.drawable.Icon;
32 import android.hardware.input.InputManager;
33 import android.hardware.input.InputManagerGlobal;
34 import android.media.AudioManager;
35 import android.metrics.LogMaker;
36 import android.os.AsyncTask;
37 import android.os.Bundle;
38 import android.os.SystemClock;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.util.TypedValue;
42 import android.view.HapticFeedbackConstants;
43 import android.view.InputDevice;
44 import android.view.KeyCharacterMap;
45 import android.view.KeyEvent;
46 import android.view.MotionEvent;
47 import android.view.SoundEffectConstants;
48 import android.view.View;
49 import android.view.ViewConfiguration;
50 import android.view.accessibility.AccessibilityEvent;
51 import android.view.accessibility.AccessibilityNodeInfo;
52 import android.widget.ImageView;
53 
54 import com.android.internal.annotations.VisibleForTesting;
55 import com.android.internal.logging.MetricsLogger;
56 import com.android.internal.logging.UiEvent;
57 import com.android.internal.logging.UiEventLogger;
58 import com.android.internal.logging.UiEventLoggerImpl;
59 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
60 import com.android.systemui.Dependency;
61 import com.android.systemui.R;
62 import com.android.systemui.recents.OverviewProxyService;
63 import com.android.systemui.shared.system.QuickStepContract;
64 
65 public class KeyButtonView extends ImageView implements ButtonInterface {
66     private static final String TAG = KeyButtonView.class.getSimpleName();
67 
68     private final boolean mPlaySounds;
69     private final UiEventLogger mUiEventLogger;
70     private int mContentDescriptionRes;
71     private long mDownTime;
72     private int mCode;
73     private int mTouchDownX;
74     private int mTouchDownY;
75     private boolean mIsVertical;
76     private AudioManager mAudioManager;
77     private boolean mGestureAborted;
78     @VisibleForTesting boolean mLongClicked;
79     private OnClickListener mOnClickListener;
80     private final KeyButtonRipple mRipple;
81     private final OverviewProxyService mOverviewProxyService;
82     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
83     private final InputManagerGlobal mInputManagerGlobal;
84     private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
85     private float mDarkIntensity;
86     private boolean mHasOvalBg = false;
87 
88     @VisibleForTesting
89     public enum NavBarButtonEvent implements UiEventLogger.UiEventEnum {
90 
91         @UiEvent(doc = "The home button was pressed in the navigation bar.")
92         NAVBAR_HOME_BUTTON_TAP(533),
93 
94         @UiEvent(doc = "The back button was pressed in the navigation bar.")
95         NAVBAR_BACK_BUTTON_TAP(534),
96 
97         @UiEvent(doc = "The overview button was pressed in the navigation bar.")
98         NAVBAR_OVERVIEW_BUTTON_TAP(535),
99 
100         @UiEvent(doc = "The ime switcher button was pressed in the navigation bar.")
101         NAVBAR_IME_SWITCHER_BUTTON_TAP(923),
102 
103         @UiEvent(doc = "The home button was long-pressed in the navigation bar.")
104         NAVBAR_HOME_BUTTON_LONGPRESS(536),
105 
106         @UiEvent(doc = "The back button was long-pressed in the navigation bar.")
107         NAVBAR_BACK_BUTTON_LONGPRESS(537),
108 
109         @UiEvent(doc = "The overview button was long-pressed in the navigation bar.")
110         NAVBAR_OVERVIEW_BUTTON_LONGPRESS(538),
111 
112         NONE(0);  // an event we should not log
113 
114         private final int mId;
115 
NavBarButtonEvent(int id)116         NavBarButtonEvent(int id) {
117             mId = id;
118         }
119 
120         @Override
getId()121         public int getId() {
122             return mId;
123         }
124     }
125     private final Runnable mCheckLongPress = new Runnable() {
126         public void run() {
127             if (isPressed()) {
128                 // Log.d("KeyButtonView", "longpressed: " + this);
129                 if (isLongClickable()) {
130                     // Just an old-fashioned ImageView
131                     performLongClick();
132                     mLongClicked = true;
133                 } else {
134                     if (mCode != KEYCODE_UNKNOWN) {
135                         sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
136                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
137                     }
138                     mLongClicked = true;
139                 }
140             }
141         }
142     };
143 
KeyButtonView(Context context, AttributeSet attrs)144     public KeyButtonView(Context context, AttributeSet attrs) {
145         this(context, attrs, 0);
146     }
147 
KeyButtonView(Context context, AttributeSet attrs, int defStyle)148     public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
149         this(context, attrs, defStyle, InputManagerGlobal.getInstance(), new UiEventLoggerImpl());
150     }
151 
152     @VisibleForTesting
KeyButtonView(Context context, AttributeSet attrs, int defStyle, InputManagerGlobal manager, UiEventLogger uiEventLogger)153     public KeyButtonView(Context context, AttributeSet attrs, int defStyle,
154             InputManagerGlobal manager, UiEventLogger uiEventLogger) {
155         super(context, attrs);
156         mUiEventLogger = uiEventLogger;
157 
158         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,
159                 defStyle, 0);
160 
161         mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, KEYCODE_UNKNOWN);
162 
163         mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);
164 
165         TypedValue value = new TypedValue();
166         if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
167             mContentDescriptionRes = value.resourceId;
168         }
169 
170         a.recycle();
171 
172         setClickable(true);
173         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
174 
175         mRipple = new KeyButtonRipple(context, this, R.dimen.key_button_ripple_max_width);
176         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
177         mInputManagerGlobal = manager;
178         setBackground(mRipple);
179         setWillNotDraw(false);
180         forceHasOverlappingRendering(false);
181     }
182 
183     @Override
isClickable()184     public boolean isClickable() {
185         return mCode != KEYCODE_UNKNOWN || super.isClickable();
186     }
187 
setCode(int code)188     public void setCode(int code) {
189         mCode = code;
190     }
191 
192     @Override
setOnClickListener(OnClickListener onClickListener)193     public void setOnClickListener(OnClickListener onClickListener) {
194         super.setOnClickListener(onClickListener);
195         mOnClickListener = onClickListener;
196     }
197 
loadAsync(Icon icon)198     public void loadAsync(Icon icon) {
199         new AsyncTask<Icon, Void, Drawable>() {
200             @Override
201             protected Drawable doInBackground(Icon... params) {
202                 return params[0].loadDrawable(mContext);
203             }
204 
205             @Override
206             protected void onPostExecute(Drawable drawable) {
207                 setImageDrawable(drawable);
208             }
209         }.execute(icon);
210     }
211 
212     @Override
onConfigurationChanged(Configuration newConfig)213     protected void onConfigurationChanged(Configuration newConfig) {
214         super.onConfigurationChanged(newConfig);
215 
216         if (mContentDescriptionRes != 0) {
217             setContentDescription(mContext.getString(mContentDescriptionRes));
218         }
219     }
220 
221     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)222     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
223         super.onInitializeAccessibilityNodeInfo(info);
224         if (mCode != KEYCODE_UNKNOWN) {
225             info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null));
226             if (isLongClickable()) {
227                 info.addAction(
228                         new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null));
229             }
230         }
231     }
232 
233     @Override
onWindowVisibilityChanged(int visibility)234     protected void onWindowVisibilityChanged(int visibility) {
235         super.onWindowVisibilityChanged(visibility);
236         if (visibility != View.VISIBLE) {
237             jumpDrawablesToCurrentState();
238         }
239     }
240 
241     @Override
performAccessibilityActionInternal(int action, Bundle arguments)242     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
243         if (action == ACTION_CLICK && mCode != KEYCODE_UNKNOWN) {
244             sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis());
245             sendEvent(KeyEvent.ACTION_UP, 0);
246             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
247             playSoundEffect(SoundEffectConstants.CLICK);
248             return true;
249         } else if (action == ACTION_LONG_CLICK && mCode != KEYCODE_UNKNOWN) {
250             sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
251             sendEvent(KeyEvent.ACTION_UP, 0);
252             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
253             return true;
254         }
255         return super.performAccessibilityActionInternal(action, arguments);
256     }
257 
258     @Override
onTouchEvent(MotionEvent ev)259     public boolean onTouchEvent(MotionEvent ev) {
260         final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI();
261         final int action = ev.getAction();
262         int x, y;
263         if (action == MotionEvent.ACTION_DOWN) {
264             mGestureAborted = false;
265         }
266         if (mGestureAborted) {
267             setPressed(false);
268             return false;
269         }
270 
271         switch (action) {
272             case MotionEvent.ACTION_DOWN:
273                 mDownTime = SystemClock.uptimeMillis();
274                 mLongClicked = false;
275                 setPressed(true);
276 
277                 mTouchDownX = (int) ev.getX();
278                 mTouchDownY = (int) ev.getY();
279                 if (mCode != KEYCODE_UNKNOWN) {
280                     sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
281                 } else {
282                     // Provide the same haptic feedback that the system offers for virtual keys.
283                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
284                 }
285                 if (!showSwipeUI) {
286                     playSoundEffect(SoundEffectConstants.CLICK);
287                 }
288                 removeCallbacks(mCheckLongPress);
289                 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
290                 break;
291             case MotionEvent.ACTION_MOVE:
292                 x = (int) ev.getX();
293                 y = (int) ev.getY();
294 
295                 float slop = QuickStepContract.getQuickStepTouchSlopPx(getContext());
296                 if (Math.abs(x - mTouchDownX) > slop || Math.abs(y - mTouchDownY) > slop) {
297                     // When quick step is enabled, prevent animating the ripple triggered by
298                     // setPressed and decide to run it on touch up
299                     setPressed(false);
300                     removeCallbacks(mCheckLongPress);
301                 }
302                 break;
303             case MotionEvent.ACTION_CANCEL:
304                 setPressed(false);
305                 if (mCode != KEYCODE_UNKNOWN) {
306                     sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
307                 }
308                 removeCallbacks(mCheckLongPress);
309                 break;
310             case MotionEvent.ACTION_UP:
311                 final boolean doIt = isPressed() && !mLongClicked;
312                 setPressed(false);
313                 final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
314                 if (showSwipeUI) {
315                     if (doIt) {
316                         // Apply haptic feedback on touch up since there is none on touch down
317                         performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
318                         playSoundEffect(SoundEffectConstants.CLICK);
319                     }
320                 } else if (doHapticFeedback && !mLongClicked) {
321                     // Always send a release ourselves because it doesn't seem to be sent elsewhere
322                     // and it feels weird to sometimes get a release haptic and other times not.
323                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
324                 }
325                 if (mCode != KEYCODE_UNKNOWN) {
326                     if (doIt) {
327                         sendEvent(KeyEvent.ACTION_UP, 0);
328                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
329                     } else {
330                         sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
331                     }
332                 } else {
333                     // no key code, just a regular ImageView
334                     if (doIt && mOnClickListener != null) {
335                         mOnClickListener.onClick(this);
336                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
337                     }
338                 }
339                 removeCallbacks(mCheckLongPress);
340                 break;
341         }
342 
343         return true;
344     }
345 
346     @Override
setImageDrawable(Drawable drawable)347     public void setImageDrawable(Drawable drawable) {
348         super.setImageDrawable(drawable);
349 
350         if (drawable == null) {
351             return;
352         }
353         KeyButtonDrawable keyButtonDrawable = (KeyButtonDrawable) drawable;
354         keyButtonDrawable.setDarkIntensity(mDarkIntensity);
355         mHasOvalBg = keyButtonDrawable.hasOvalBg();
356         if (mHasOvalBg) {
357             mOvalBgPaint.setColor(keyButtonDrawable.getDrawableBackgroundColor());
358         }
359         mRipple.setType(keyButtonDrawable.hasOvalBg() ? KeyButtonRipple.Type.OVAL
360                 : KeyButtonRipple.Type.ROUNDED_RECT);
361     }
362 
playSoundEffect(int soundConstant)363     public void playSoundEffect(int soundConstant) {
364         if (!mPlaySounds) return;
365         mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser());
366     }
367 
sendEvent(int action, int flags)368     public void sendEvent(int action, int flags) {
369         sendEvent(action, flags, SystemClock.uptimeMillis());
370     }
371 
logSomePresses(int action, int flags)372     private void logSomePresses(int action, int flags) {
373         boolean longPressSet = (flags & KeyEvent.FLAG_LONG_PRESS) != 0;
374         NavBarButtonEvent uiEvent = NavBarButtonEvent.NONE;
375         if (action == MotionEvent.ACTION_UP && mLongClicked) {
376             return;  // don't log the up after a long press
377         }
378         if (action == MotionEvent.ACTION_DOWN && !longPressSet) {
379             return;  // don't log a down unless it is also the long press marker
380         }
381         if ((flags & KeyEvent.FLAG_CANCELED) != 0
382                 || (flags & KeyEvent.FLAG_CANCELED_LONG_PRESS) != 0) {
383             return;  // don't log various cancels
384         }
385         switch(mCode) {
386             case KeyEvent.KEYCODE_BACK:
387                 uiEvent = longPressSet
388                         ? NavBarButtonEvent.NAVBAR_BACK_BUTTON_LONGPRESS
389                         : NavBarButtonEvent.NAVBAR_BACK_BUTTON_TAP;
390                 break;
391             case KeyEvent.KEYCODE_HOME:
392                 uiEvent = longPressSet
393                         ? NavBarButtonEvent.NAVBAR_HOME_BUTTON_LONGPRESS
394                         : NavBarButtonEvent.NAVBAR_HOME_BUTTON_TAP;
395                 break;
396             case KeyEvent.KEYCODE_APP_SWITCH:
397                 uiEvent = longPressSet
398                         ? NavBarButtonEvent.NAVBAR_OVERVIEW_BUTTON_LONGPRESS
399                         : NavBarButtonEvent.NAVBAR_OVERVIEW_BUTTON_TAP;
400                 break;
401         }
402         if (uiEvent != NavBarButtonEvent.NONE) {
403             mUiEventLogger.log(uiEvent);
404         }
405     }
406 
sendEvent(int action, int flags, long when)407     private void sendEvent(int action, int flags, long when) {
408         mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
409                 .setType(MetricsEvent.TYPE_ACTION)
410                 .setSubtype(mCode)
411                 .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
412                 .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
413         logSomePresses(action, flags);
414         if (mCode == KeyEvent.KEYCODE_BACK && flags != KeyEvent.FLAG_LONG_PRESS) {
415             Log.i(TAG, "Back button event: " + KeyEvent.actionToString(action));
416         }
417         final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
418         final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
419                 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
420                 flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
421                 InputDevice.SOURCE_KEYBOARD);
422 
423         int displayId = INVALID_DISPLAY;
424 
425         // Make KeyEvent work on multi-display environment
426         if (getDisplay() != null) {
427             displayId = getDisplay().getDisplayId();
428         }
429         if (displayId != INVALID_DISPLAY) {
430             ev.setDisplayId(displayId);
431         }
432         mInputManagerGlobal.injectInputEvent(ev,
433                 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
434     }
435 
436     @Override
abortCurrentGesture()437     public void abortCurrentGesture() {
438         Log.d("b/63783866", "KeyButtonView.abortCurrentGesture");
439         if (mCode != KeyEvent.KEYCODE_UNKNOWN) {
440             sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
441         }
442         setPressed(false);
443         mRipple.abortDelayedRipple();
444         mGestureAborted = true;
445     }
446 
447     @Override
setDarkIntensity(float darkIntensity)448     public void setDarkIntensity(float darkIntensity) {
449         mDarkIntensity = darkIntensity;
450 
451         Drawable drawable = getDrawable();
452         if (drawable != null) {
453             ((KeyButtonDrawable) drawable).setDarkIntensity(darkIntensity);
454             // Since we reuse the same drawable for multiple views, we need to invalidate the view
455             // manually.
456             invalidate();
457         }
458         mRipple.setDarkIntensity(darkIntensity);
459     }
460 
461     @Override
setDelayTouchFeedback(boolean shouldDelay)462     public void setDelayTouchFeedback(boolean shouldDelay) {
463         mRipple.setDelayTouchFeedback(shouldDelay);
464     }
465 
466     @Override
draw(Canvas canvas)467     public void draw(Canvas canvas) {
468         if (mHasOvalBg) {
469             int d = Math.min(getWidth(), getHeight());
470             canvas.drawOval(0, 0, d, d, mOvalBgPaint);
471         }
472         super.draw(canvas);
473     }
474 
475     @Override
setVertical(boolean vertical)476     public void setVertical(boolean vertical) {
477         mIsVertical = vertical;
478     }
479 }
480