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