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