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