Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2016 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 | package com.android.car; |
| 17 | |
| 18 | import android.content.Context; |
| 19 | import android.content.Intent; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 20 | import android.net.Uri; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 21 | import android.os.ParcelFileDescriptor; |
| 22 | import android.os.SystemClock; |
| 23 | import android.os.UserHandle; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 24 | import android.provider.CallLog.Calls; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 25 | import android.speech.RecognizerIntent; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 26 | import android.telecom.TelecomManager; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 27 | import android.util.Log; |
| 28 | import android.view.KeyEvent; |
| 29 | |
| 30 | import com.android.car.hal.InputHalService; |
| 31 | import com.android.car.hal.VehicleHal; |
| 32 | |
| 33 | import java.io.File; |
| 34 | import java.io.FileNotFoundException; |
| 35 | import java.io.IOException; |
| 36 | import java.io.PrintWriter; |
| 37 | |
| 38 | public class CarInputService implements CarServiceBase, InputHalService.InputListener { |
| 39 | |
| 40 | public interface KeyEventListener { |
Yao Chen | c4d442f | 2016-04-08 11:33:47 -0700 | [diff] [blame] | 41 | boolean onKeyEvent(KeyEvent event); |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 42 | } |
| 43 | |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 44 | private static final long LONG_PRESS_TIME_MS = 1000; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 45 | |
| 46 | private final Context mContext; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 47 | private final TelecomManager mTelecomManager; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 48 | |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 49 | private KeyEventListener mVoiceAssistantKeyListener; |
| 50 | private KeyEventListener mLongVoiceAssistantKeyListener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 51 | private long mLastVoiceKeyDownTime = 0; |
| 52 | |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 53 | private long mLastCallKeyDownTime = 0; |
| 54 | |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 55 | private KeyEventListener mInstrumentClusterKeyListener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 56 | |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 57 | private KeyEventListener mVolumeKeyListener; |
| 58 | |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 59 | private ParcelFileDescriptor mInjectionDeviceFd; |
| 60 | |
| 61 | private int mKeyEventCount = 0; |
| 62 | |
| 63 | public CarInputService(Context context) { |
| 64 | mContext = context; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 65 | mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 66 | } |
| 67 | |
| 68 | /** |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 69 | * Set listener for listening voice assistant key event. Setting to null stops listening. |
| 70 | * If listener is not set, default behavior will be done for short press. |
| 71 | * If listener is set, short key press will lead into calling the listener. |
| 72 | * @param listener |
| 73 | */ |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 74 | public void setVoiceAssistantKeyListener(KeyEventListener listener) { |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 75 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 76 | mVoiceAssistantKeyListener = listener; |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 77 | } |
| 78 | } |
| 79 | |
| 80 | /** |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 81 | * Set listener for listening long voice assistant key event. Setting to null stops listening. |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 82 | * If listener is not set, default behavior will be done for long press. |
| 83 | * If listener is set, short long press will lead into calling the listener. |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 84 | * @param listener |
| 85 | */ |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 86 | public void setLongVoiceAssistantKeyListener(KeyEventListener listener) { |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 87 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 88 | mLongVoiceAssistantKeyListener = listener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 89 | } |
| 90 | } |
| 91 | |
| 92 | public void setInstrumentClusterKeyListener(KeyEventListener listener) { |
| 93 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 94 | mInstrumentClusterKeyListener = listener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 95 | } |
| 96 | } |
| 97 | |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 98 | public void setVolumeKeyListener(KeyEventListener listener) { |
| 99 | synchronized (this) { |
| 100 | mVolumeKeyListener = listener; |
| 101 | } |
| 102 | } |
| 103 | |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 104 | @Override |
| 105 | public void init() { |
| 106 | InputHalService hal = VehicleHal.getInstance().getInputHal(); |
| 107 | if (!hal.isKeyInputSupported()) { |
| 108 | Log.w(CarLog.TAG_INPUT, "Hal does not support key input."); |
| 109 | return; |
| 110 | } |
| 111 | String injectionDevice = mContext.getResources().getString( |
| 112 | R.string.inputInjectionDeviceNode); |
| 113 | ParcelFileDescriptor file = null; |
| 114 | try { |
| 115 | file = ParcelFileDescriptor.open(new File(injectionDevice), |
| 116 | ParcelFileDescriptor.MODE_READ_WRITE); |
| 117 | } catch (FileNotFoundException e) { |
Pavel Maltsev | f61ae28 | 2016-03-25 17:33:50 +0000 | [diff] [blame] | 118 | Log.w(CarLog.TAG_INPUT, "cannot open device for input injection:" + injectionDevice); |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 119 | return; |
| 120 | } |
| 121 | synchronized (this) { |
| 122 | mInjectionDeviceFd = file; |
| 123 | } |
| 124 | hal.setInputListener(this); |
| 125 | } |
| 126 | |
| 127 | @Override |
| 128 | public void release() { |
| 129 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 130 | mVoiceAssistantKeyListener = null; |
| 131 | mLongVoiceAssistantKeyListener = null; |
| 132 | mInstrumentClusterKeyListener = null; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 133 | if (mInjectionDeviceFd != null) { |
| 134 | try { |
| 135 | mInjectionDeviceFd.close(); |
| 136 | } catch (IOException e) { |
| 137 | } |
| 138 | } |
| 139 | mInjectionDeviceFd = null; |
| 140 | mKeyEventCount = 0; |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | @Override |
| 145 | public void onKeyEvent(KeyEvent event, int targetDisplay) { |
| 146 | synchronized (this) { |
| 147 | mKeyEventCount++; |
| 148 | } |
| 149 | int keyCode = event.getKeyCode(); |
| 150 | switch (keyCode) { |
| 151 | case KeyEvent.KEYCODE_VOICE_ASSIST: |
| 152 | handleVoiceAssistKey(event); |
| 153 | return; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 154 | case KeyEvent.KEYCODE_CALL: |
| 155 | handleCallKey(event); |
| 156 | return; |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 157 | case KeyEvent.KEYCODE_VOLUME_UP: |
| 158 | case KeyEvent.KEYCODE_VOLUME_DOWN: |
| 159 | handleVolumeKey(event); |
| 160 | return; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 161 | default: |
| 162 | break; |
| 163 | } |
| 164 | if (targetDisplay == InputHalService.DISPLAY_INSTRUMENT_CLUSTER) { |
| 165 | handleInstrumentClusterKey(event); |
| 166 | } else { |
| 167 | handleMainDisplayKey(event); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | private void handleVoiceAssistKey(KeyEvent event) { |
| 172 | int action = event.getAction(); |
| 173 | if (action == KeyEvent.ACTION_DOWN) { |
| 174 | long now = SystemClock.elapsedRealtime(); |
| 175 | synchronized (this) { |
| 176 | mLastVoiceKeyDownTime = now; |
| 177 | } |
| 178 | } else if (action == KeyEvent.ACTION_UP) { |
| 179 | // if no listener, do not handle long press |
| 180 | KeyEventListener listener = null; |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 181 | KeyEventListener shortPressListener = null; |
| 182 | KeyEventListener longPressListener = null; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 183 | long downTime; |
| 184 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 185 | shortPressListener = mVoiceAssistantKeyListener; |
| 186 | longPressListener = mLongVoiceAssistantKeyListener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 187 | downTime = mLastVoiceKeyDownTime; |
| 188 | } |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 189 | if (shortPressListener == null && longPressListener == null) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 190 | launchDefaultVoiceAssistantHandler(); |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 191 | } else { |
| 192 | long duration = SystemClock.elapsedRealtime() - downTime; |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 193 | listener = (duration > LONG_PRESS_TIME_MS |
Vitalii Tomkiv | 61e2c23 | 2016-03-08 10:48:51 -0800 | [diff] [blame] | 194 | ? longPressListener : shortPressListener); |
| 195 | if (listener != null) { |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 196 | listener.onKeyEvent(event); |
| 197 | } else { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 198 | launchDefaultVoiceAssistantHandler(); |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 199 | } |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
Vitalii Tomkiv | e2142e5 | 2016-04-29 11:35:26 -0700 | [diff] [blame] | 204 | private void handleCallKey(KeyEvent event) { |
| 205 | int action = event.getAction(); |
| 206 | if (action == KeyEvent.ACTION_DOWN) { |
| 207 | // Only handle if it's ringing when button down. |
| 208 | if (mTelecomManager != null && mTelecomManager.isRinging()) { |
| 209 | Log.i(CarLog.TAG_INPUT, "call key while rinning. Answer the call!"); |
| 210 | mTelecomManager.acceptRingingCall(); |
| 211 | return; |
| 212 | } |
| 213 | |
| 214 | long now = SystemClock.elapsedRealtime(); |
| 215 | synchronized (this) { |
| 216 | mLastCallKeyDownTime = now; |
| 217 | } |
| 218 | } else if (action == KeyEvent.ACTION_UP) { |
| 219 | long downTime; |
| 220 | synchronized (this) { |
| 221 | downTime = mLastCallKeyDownTime; |
| 222 | } |
| 223 | long duration = SystemClock.elapsedRealtime() - downTime; |
| 224 | if (duration > LONG_PRESS_TIME_MS) { |
| 225 | dialLastCallHandler(); |
| 226 | } else { |
| 227 | launchDialerHandler(); |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | private void launchDialerHandler() { |
| 233 | Log.i(CarLog.TAG_INPUT, "call key, launch dialer intent"); |
| 234 | Intent dialerIntent = new Intent(Intent.ACTION_DIAL); |
| 235 | mContext.startActivityAsUser(dialerIntent, null, UserHandle.CURRENT_OR_SELF); |
| 236 | } |
| 237 | |
| 238 | private void dialLastCallHandler() { |
| 239 | Log.i(CarLog.TAG_INPUT, "call key, dialing last call"); |
| 240 | |
| 241 | String lastNumber = Calls.getLastOutgoingCall(mContext); |
| 242 | Log.d(CarLog.TAG_INPUT, "Last number dialed: " + lastNumber); |
| 243 | if (lastNumber != null && !lastNumber.isEmpty()) { |
| 244 | Intent callLastNumberIntent = new Intent(Intent.ACTION_CALL) |
| 245 | .setData(Uri.fromParts("tel", lastNumber, null)) |
| 246 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 247 | mContext.startActivityAsUser(callLastNumberIntent, null, UserHandle.CURRENT_OR_SELF); |
| 248 | } |
| 249 | } |
| 250 | |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 251 | private void launchDefaultVoiceAssistantHandler() { |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 252 | Log.i(CarLog.TAG_INPUT, "voice key, launch default intent"); |
| 253 | Intent voiceIntent = |
| 254 | new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE); |
| 255 | mContext.startActivityAsUser(voiceIntent, null, UserHandle.CURRENT_OR_SELF); |
| 256 | } |
| 257 | |
| 258 | private void handleInstrumentClusterKey(KeyEvent event) { |
| 259 | KeyEventListener listener = null; |
| 260 | synchronized (this) { |
Pavel Maltsev | e11ad3a | 2016-03-25 15:40:24 -0700 | [diff] [blame] | 261 | listener = mInstrumentClusterKeyListener; |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 262 | } |
| 263 | if (listener == null) { |
| 264 | return; |
| 265 | } |
| 266 | listener.onKeyEvent(event); |
| 267 | } |
| 268 | |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 269 | private void handleVolumeKey(KeyEvent event) { |
Yao Chen | c4d442f | 2016-04-08 11:33:47 -0700 | [diff] [blame] | 270 | KeyEventListener listener; |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 271 | synchronized (this) { |
| 272 | listener = mVolumeKeyListener; |
| 273 | } |
Yao Chen | c4d442f | 2016-04-08 11:33:47 -0700 | [diff] [blame] | 274 | if (listener != null) { |
| 275 | listener.onKeyEvent(event); |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 276 | } |
Keun-young Park | 07182c7 | 2016-03-18 18:01:29 -0700 | [diff] [blame] | 277 | } |
| 278 | |
Keun-young Park | a28d7b2 | 2016-02-29 16:54:29 -0800 | [diff] [blame] | 279 | private void handleMainDisplayKey(KeyEvent event) { |
| 280 | int fd; |
| 281 | synchronized (this) { |
| 282 | fd = mInjectionDeviceFd.getFd(); |
| 283 | } |
| 284 | int action = event.getAction(); |
| 285 | boolean isDown = (action == KeyEvent.ACTION_DOWN); |
| 286 | int keyCode = event.getKeyCode(); |
| 287 | int r = nativeInjectKeyEvent(fd, keyCode, isDown); |
| 288 | if (r != 0) { |
| 289 | Log.e(CarLog.TAG_INPUT, "cannot inject key event, failed with:" + r); |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | @Override |
| 294 | public void dump(PrintWriter writer) { |
| 295 | writer.println("*Input Service*"); |
| 296 | writer.println("mInjectionDeviceFd:" + mInjectionDeviceFd); |
| 297 | writer.println("mLastVoiceKeyDownTime:" + mLastVoiceKeyDownTime + |
| 298 | ",mKeyEventCount:" + mKeyEventCount); |
| 299 | } |
| 300 | |
| 301 | private native int nativeInjectKeyEvent(int fd, int keyCode, boolean isDown); |
| 302 | } |