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