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