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.accessibility.AccessibilityNodeInfo.ACTION_CLICK; 20 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; 21 22 import android.app.ActivityManager; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.TypedArray; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.Icon; 28 import android.hardware.input.InputManager; 29 import android.media.AudioManager; 30 import android.metrics.LogMaker; 31 import android.os.AsyncTask; 32 import android.os.Bundle; 33 import android.os.SystemClock; 34 import android.util.AttributeSet; 35 import android.util.TypedValue; 36 import android.view.HapticFeedbackConstants; 37 import android.view.InputDevice; 38 import android.view.KeyCharacterMap; 39 import android.view.KeyEvent; 40 import android.view.MotionEvent; 41 import android.view.SoundEffectConstants; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.accessibility.AccessibilityEvent; 45 import android.view.accessibility.AccessibilityNodeInfo; 46 import android.widget.ImageView; 47 import com.android.internal.logging.MetricsLogger; 48 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 49 import com.android.systemui.Dependency; 50 import com.android.systemui.OverviewProxyService; 51 import com.android.systemui.R; 52 import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface; 53 import com.android.systemui.shared.system.NavigationBarCompat; 54 55 public class KeyButtonView extends ImageView implements ButtonInterface { 56 private static final String TAG = KeyButtonView.class.getSimpleName(); 57 58 private final boolean mPlaySounds; 59 private int mContentDescriptionRes; 60 private long mDownTime; 61 private int mCode; 62 private int mTouchDownX; 63 private int mTouchDownY; 64 private boolean mIsVertical; 65 private boolean mSupportsLongpress = true; 66 private AudioManager mAudioManager; 67 private boolean mGestureAborted; 68 private boolean mLongClicked; 69 private OnClickListener mOnClickListener; 70 private final KeyButtonRipple mRipple; 71 private final OverviewProxyService mOverviewProxyService; 72 private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 73 74 private final Runnable mCheckLongPress = new Runnable() { 75 public void run() { 76 if (isPressed()) { 77 // Log.d("KeyButtonView", "longpressed: " + this); 78 if (isLongClickable()) { 79 // Just an old-fashioned ImageView 80 performLongClick(); 81 mLongClicked = true; 82 } else if (mSupportsLongpress) { 83 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); 84 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 85 mLongClicked = true; 86 } 87 } 88 } 89 }; 90 KeyButtonView(Context context, AttributeSet attrs)91 public KeyButtonView(Context context, AttributeSet attrs) { 92 this(context, attrs, 0); 93 } 94 KeyButtonView(Context context, AttributeSet attrs, int defStyle)95 public KeyButtonView(Context context, AttributeSet attrs, int defStyle) { 96 super(context, attrs); 97 98 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView, 99 defStyle, 0); 100 101 mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0); 102 103 mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true); 104 mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true); 105 106 TypedValue value = new TypedValue(); 107 if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) { 108 mContentDescriptionRes = value.resourceId; 109 } 110 111 a.recycle(); 112 113 setClickable(true); 114 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 115 116 mRipple = new KeyButtonRipple(context, this); 117 mOverviewProxyService = Dependency.get(OverviewProxyService.class); 118 setBackground(mRipple); 119 forceHasOverlappingRendering(false); 120 } 121 122 @Override isClickable()123 public boolean isClickable() { 124 return mCode != 0 || super.isClickable(); 125 } 126 setCode(int code)127 public void setCode(int code) { 128 mCode = code; 129 } 130 131 @Override setOnClickListener(OnClickListener onClickListener)132 public void setOnClickListener(OnClickListener onClickListener) { 133 super.setOnClickListener(onClickListener); 134 mOnClickListener = onClickListener; 135 } 136 loadAsync(Icon icon)137 public void loadAsync(Icon icon) { 138 new AsyncTask<Icon, Void, Drawable>() { 139 @Override 140 protected Drawable doInBackground(Icon... params) { 141 return params[0].loadDrawable(mContext); 142 } 143 144 @Override 145 protected void onPostExecute(Drawable drawable) { 146 setImageDrawable(drawable); 147 } 148 }.execute(icon); 149 } 150 151 @Override onConfigurationChanged(Configuration newConfig)152 protected void onConfigurationChanged(Configuration newConfig) { 153 super.onConfigurationChanged(newConfig); 154 155 if (mContentDescriptionRes != 0) { 156 setContentDescription(mContext.getString(mContentDescriptionRes)); 157 } 158 } 159 160 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)161 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 162 super.onInitializeAccessibilityNodeInfo(info); 163 if (mCode != 0) { 164 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null)); 165 if (mSupportsLongpress || isLongClickable()) { 166 info.addAction( 167 new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null)); 168 } 169 } 170 } 171 172 @Override onWindowVisibilityChanged(int visibility)173 protected void onWindowVisibilityChanged(int visibility) { 174 super.onWindowVisibilityChanged(visibility); 175 if (visibility != View.VISIBLE) { 176 jumpDrawablesToCurrentState(); 177 } 178 } 179 180 @Override performAccessibilityActionInternal(int action, Bundle arguments)181 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 182 if (action == ACTION_CLICK && mCode != 0) { 183 sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis()); 184 sendEvent(KeyEvent.ACTION_UP, 0); 185 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 186 playSoundEffect(SoundEffectConstants.CLICK); 187 return true; 188 } else if (action == ACTION_LONG_CLICK && mCode != 0) { 189 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); 190 sendEvent(KeyEvent.ACTION_UP, 0); 191 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 192 return true; 193 } 194 return super.performAccessibilityActionInternal(action, arguments); 195 } 196 onTouchEvent(MotionEvent ev)197 public boolean onTouchEvent(MotionEvent ev) { 198 final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI(); 199 final int action = ev.getAction(); 200 int x, y; 201 if (action == MotionEvent.ACTION_DOWN) { 202 mGestureAborted = false; 203 } 204 if (mGestureAborted) { 205 setPressed(false); 206 return false; 207 } 208 209 switch (action) { 210 case MotionEvent.ACTION_DOWN: 211 mDownTime = SystemClock.uptimeMillis(); 212 mLongClicked = false; 213 setPressed(true); 214 215 // Use raw X and Y to detect gestures in case a parent changes the x and y values 216 mTouchDownX = (int) ev.getRawX(); 217 mTouchDownY = (int) ev.getRawY(); 218 if (mCode != 0) { 219 sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); 220 } else { 221 // Provide the same haptic feedback that the system offers for virtual keys. 222 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 223 } 224 if (!showSwipeUI) { 225 playSoundEffect(SoundEffectConstants.CLICK); 226 } 227 removeCallbacks(mCheckLongPress); 228 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); 229 break; 230 case MotionEvent.ACTION_MOVE: 231 x = (int)ev.getRawX(); 232 y = (int)ev.getRawY(); 233 234 boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) > (mIsVertical 235 ? NavigationBarCompat.getQuickScrubTouchSlopPx() 236 : NavigationBarCompat.getQuickStepTouchSlopPx()); 237 boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) > (mIsVertical 238 ? NavigationBarCompat.getQuickStepTouchSlopPx() 239 : NavigationBarCompat.getQuickScrubTouchSlopPx()); 240 if (exceededTouchSlopX || exceededTouchSlopY) { 241 // When quick step is enabled, prevent animating the ripple triggered by 242 // setPressed and decide to run it on touch up 243 setPressed(false); 244 removeCallbacks(mCheckLongPress); 245 } 246 break; 247 case MotionEvent.ACTION_CANCEL: 248 setPressed(false); 249 if (mCode != 0) { 250 sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); 251 } 252 removeCallbacks(mCheckLongPress); 253 break; 254 case MotionEvent.ACTION_UP: 255 final boolean doIt = isPressed() && !mLongClicked; 256 setPressed(false); 257 final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150; 258 if (showSwipeUI) { 259 if (doIt) { 260 // Apply haptic feedback on touch up since there is none on touch down 261 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 262 playSoundEffect(SoundEffectConstants.CLICK); 263 } 264 } else if (doHapticFeedback && !mLongClicked) { 265 // Always send a release ourselves because it doesn't seem to be sent elsewhere 266 // and it feels weird to sometimes get a release haptic and other times not. 267 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE); 268 } 269 if (mCode != 0) { 270 if (doIt) { 271 sendEvent(KeyEvent.ACTION_UP, 0); 272 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 273 } else { 274 sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); 275 } 276 } else { 277 // no key code, just a regular ImageView 278 if (doIt && mOnClickListener != null) { 279 mOnClickListener.onClick(this); 280 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 281 } 282 } 283 removeCallbacks(mCheckLongPress); 284 break; 285 } 286 287 return true; 288 } 289 playSoundEffect(int soundConstant)290 public void playSoundEffect(int soundConstant) { 291 if (!mPlaySounds) return; 292 mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser()); 293 } 294 sendEvent(int action, int flags)295 public void sendEvent(int action, int flags) { 296 sendEvent(action, flags, SystemClock.uptimeMillis()); 297 } 298 sendEvent(int action, int flags, long when)299 void sendEvent(int action, int flags, long when) { 300 mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT) 301 .setType(MetricsEvent.TYPE_ACTION) 302 .setSubtype(mCode) 303 .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action) 304 .addTaggedData(MetricsEvent.FIELD_FLAGS, flags)); 305 final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0; 306 final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, 307 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 308 flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, 309 InputDevice.SOURCE_KEYBOARD); 310 InputManager.getInstance().injectInputEvent(ev, 311 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 312 } 313 314 @Override abortCurrentGesture()315 public void abortCurrentGesture() { 316 setPressed(false); 317 mRipple.abortDelayedRipple(); 318 mGestureAborted = true; 319 } 320 321 @Override setDarkIntensity(float darkIntensity)322 public void setDarkIntensity(float darkIntensity) { 323 Drawable drawable = getDrawable(); 324 if (drawable != null) { 325 ((KeyButtonDrawable) getDrawable()).setDarkIntensity(darkIntensity); 326 327 // Since we reuse the same drawable for multiple views, we need to invalidate the view 328 // manually. 329 invalidate(); 330 } 331 mRipple.setDarkIntensity(darkIntensity); 332 } 333 334 @Override setDelayTouchFeedback(boolean shouldDelay)335 public void setDelayTouchFeedback(boolean shouldDelay) { 336 mRipple.setDelayTouchFeedback(shouldDelay); 337 } 338 339 @Override setVertical(boolean vertical)340 public void setVertical(boolean vertical) { 341 mIsVertical = vertical; 342 } 343 } 344 345 346