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