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