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 static android.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; 19 import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_PUSH_TO_TALK; 20 21 import android.annotation.Nullable; 22 import android.app.ActivityManager; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothHeadsetClient; 26 import android.bluetooth.BluetoothProfile; 27 import android.car.CarProjectionManager; 28 import android.car.input.CarInputHandlingService; 29 import android.car.input.CarInputHandlingService.InputFilter; 30 import android.car.input.ICarInputListener; 31 import android.content.ComponentName; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.ServiceConnection; 36 import android.hardware.input.InputManager; 37 import android.net.Uri; 38 import android.os.Binder; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.IBinder; 42 import android.os.Looper; 43 import android.os.Parcel; 44 import android.os.RemoteException; 45 import android.os.UserHandle; 46 import android.provider.CallLog.Calls; 47 import android.provider.Settings; 48 import android.telecom.TelecomManager; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.view.KeyEvent; 52 import android.view.ViewConfiguration; 53 54 import com.android.car.hal.InputHalService; 55 import com.android.internal.annotations.GuardedBy; 56 import com.android.internal.annotations.VisibleForTesting; 57 import com.android.internal.app.AssistUtils; 58 import com.android.internal.app.IVoiceInteractionSessionShowCallback; 59 60 import java.io.PrintWriter; 61 import java.util.BitSet; 62 import java.util.List; 63 import java.util.function.IntSupplier; 64 import java.util.function.Supplier; 65 66 public class CarInputService implements CarServiceBase, InputHalService.InputListener { 67 68 /** An interface to receive {@link KeyEvent}s as they occur. */ 69 public interface KeyEventListener { 70 /** Called when a key event occurs. */ onKeyEvent(KeyEvent event)71 void onKeyEvent(KeyEvent event); 72 } 73 74 private static final class KeyPressTimer { 75 private final Handler mHandler; 76 private final Runnable mLongPressRunnable; 77 private final Runnable mCallback = this::onTimerExpired; 78 private final IntSupplier mLongPressDelaySupplier; 79 80 @GuardedBy("this") 81 private boolean mDown = false; 82 @GuardedBy("this") 83 private boolean mLongPress = false; 84 KeyPressTimer( Handler handler, IntSupplier longPressDelaySupplier, Runnable longPressRunnable)85 KeyPressTimer( 86 Handler handler, IntSupplier longPressDelaySupplier, Runnable longPressRunnable) { 87 mHandler = handler; 88 mLongPressRunnable = longPressRunnable; 89 mLongPressDelaySupplier = longPressDelaySupplier; 90 } 91 92 /** Marks that a key was pressed, and starts the long-press timer. */ keyDown()93 synchronized void keyDown() { 94 mDown = true; 95 mLongPress = false; 96 mHandler.removeCallbacks(mCallback); 97 mHandler.postDelayed(mCallback, mLongPressDelaySupplier.getAsInt()); 98 } 99 100 /** 101 * Marks that a key was released, and stops the long-press timer. 102 * 103 * Returns true if the press was a long-press. 104 */ keyUp()105 synchronized boolean keyUp() { 106 mHandler.removeCallbacks(mCallback); 107 mDown = false; 108 return mLongPress; 109 } 110 onTimerExpired()111 private void onTimerExpired() { 112 synchronized (this) { 113 // If the timer expires after key-up, don't retroactively make the press long. 114 if (!mDown) { 115 return; 116 } 117 mLongPress = true; 118 } 119 120 mLongPressRunnable.run(); 121 } 122 } 123 124 private final IVoiceInteractionSessionShowCallback mShowCallback = 125 new IVoiceInteractionSessionShowCallback.Stub() { 126 @Override 127 public void onFailed() { 128 Log.w(CarLog.TAG_INPUT, "Failed to show VoiceInteractionSession"); 129 } 130 131 @Override 132 public void onShown() { 133 if (DBG) { 134 Log.d(CarLog.TAG_INPUT, "IVoiceInteractionSessionShowCallback onShown()"); 135 } 136 } 137 }; 138 139 private static final boolean DBG = false; 140 @VisibleForTesting 141 static final String EXTRA_CAR_PUSH_TO_TALK = 142 "com.android.car.input.EXTRA_CAR_PUSH_TO_TALK"; 143 144 private final Context mContext; 145 private final InputHalService mInputHalService; 146 private final TelecomManager mTelecomManager; 147 private final AssistUtils mAssistUtils; 148 // The ComponentName of the CarInputListener service. Can be changed via resource overlay, 149 // or overridden directly for testing. 150 @Nullable 151 private final ComponentName mCustomInputServiceComponent; 152 // The default handler for main-display input events. By default, injects the events into 153 // the input queue via InputManager, but can be overridden for testing. 154 private final KeyEventListener mMainDisplayHandler; 155 // The supplier for the last-called number. By default, gets the number from the call log. 156 // May be overridden for testing. 157 private final Supplier<String> mLastCalledNumberSupplier; 158 // The supplier for the system long-press delay, in milliseconds. By default, gets the value 159 // from Settings.Secure for the current user, falling back to the system-wide default 160 // long-press delay defined in ViewConfiguration. May be overridden for testing. 161 private final IntSupplier mLongPressDelaySupplier; 162 163 @GuardedBy("this") 164 private CarProjectionManager.ProjectionKeyEventHandler mProjectionKeyEventHandler; 165 @GuardedBy("this") 166 private final BitSet mProjectionKeyEventsSubscribed = new BitSet(); 167 168 private final KeyPressTimer mVoiceKeyTimer; 169 private final KeyPressTimer mCallKeyTimer; 170 171 @GuardedBy("this") 172 private KeyEventListener mInstrumentClusterKeyListener; 173 174 @GuardedBy("this") 175 @VisibleForTesting 176 ICarInputListener mCarInputListener; 177 178 @GuardedBy("this") 179 private boolean mCarInputListenerBound = false; 180 181 // Maps display -> keycodes handled. 182 @GuardedBy("this") 183 private final SetMultimap<Integer, Integer> mHandledKeys = new SetMultimap<>(); 184 185 private final Binder mCallback = new Binder() { 186 @Override 187 protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { 188 if (code == CarInputHandlingService.INPUT_CALLBACK_BINDER_CODE) { 189 data.setDataPosition(0); 190 InputFilter[] handledKeys = (InputFilter[]) data.createTypedArray( 191 InputFilter.CREATOR); 192 if (handledKeys != null) { 193 setHandledKeys(handledKeys); 194 } 195 return true; 196 } 197 return false; 198 } 199 }; 200 201 private final ServiceConnection mInputServiceConnection = new ServiceConnection() { 202 @Override 203 public void onServiceConnected(ComponentName name, IBinder binder) { 204 if (DBG) { 205 Log.d(CarLog.TAG_INPUT, "onServiceConnected, name: " 206 + name + ", binder: " + binder); 207 } 208 synchronized (CarInputService.this) { 209 mCarInputListener = ICarInputListener.Stub.asInterface(binder); 210 } 211 } 212 213 @Override 214 public void onServiceDisconnected(ComponentName name) { 215 Log.d(CarLog.TAG_INPUT, "onServiceDisconnected, name: " + name); 216 synchronized (CarInputService.this) { 217 mCarInputListener = null; 218 } 219 } 220 }; 221 222 private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 223 224 // BluetoothHeadsetClient set through mBluetoothProfileServiceListener, and used by 225 // launchBluetoothVoiceRecognition(). 226 @GuardedBy("mBluetoothProfileServiceListener") 227 private BluetoothHeadsetClient mBluetoothHeadsetClient; 228 229 private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = 230 new BluetoothProfile.ServiceListener() { 231 @Override 232 public void onServiceConnected(int profile, BluetoothProfile proxy) { 233 if (profile == BluetoothProfile.HEADSET_CLIENT) { 234 Log.d(CarLog.TAG_INPUT, "Bluetooth proxy connected for HEADSET_CLIENT profile"); 235 synchronized (this) { 236 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; 237 } 238 } 239 } 240 241 @Override 242 public void onServiceDisconnected(int profile) { 243 if (profile == BluetoothProfile.HEADSET_CLIENT) { 244 Log.d(CarLog.TAG_INPUT, "Bluetooth proxy disconnected for HEADSET_CLIENT profile"); 245 synchronized (this) { 246 mBluetoothHeadsetClient = null; 247 } 248 } 249 } 250 }; 251 252 @Nullable getDefaultInputComponent(Context context)253 private static ComponentName getDefaultInputComponent(Context context) { 254 String carInputService = context.getString(R.string.inputService); 255 if (TextUtils.isEmpty(carInputService)) { 256 return null; 257 } 258 259 return ComponentName.unflattenFromString(carInputService); 260 } 261 getViewLongPressDelay(ContentResolver cr)262 private static int getViewLongPressDelay(ContentResolver cr) { 263 return Settings.Secure.getIntForUser( 264 cr, 265 Settings.Secure.LONG_PRESS_TIMEOUT, 266 ViewConfiguration.getLongPressTimeout(), 267 UserHandle.USER_CURRENT); 268 } 269 CarInputService(Context context, InputHalService inputHalService)270 public CarInputService(Context context, InputHalService inputHalService) { 271 this(context, inputHalService, new Handler(Looper.getMainLooper()), 272 context.getSystemService(TelecomManager.class), new AssistUtils(context), 273 event -> 274 context.getSystemService(InputManager.class) 275 .injectInputEvent(event, INJECT_INPUT_EVENT_MODE_ASYNC), 276 () -> Calls.getLastOutgoingCall(context), 277 getDefaultInputComponent(context), 278 () -> getViewLongPressDelay(context.getContentResolver())); 279 } 280 281 @VisibleForTesting CarInputService(Context context, InputHalService inputHalService, Handler handler, TelecomManager telecomManager, AssistUtils assistUtils, KeyEventListener mainDisplayHandler, Supplier<String> lastCalledNumberSupplier, @Nullable ComponentName customInputServiceComponent, IntSupplier longPressDelaySupplier)282 CarInputService(Context context, InputHalService inputHalService, Handler handler, 283 TelecomManager telecomManager, AssistUtils assistUtils, 284 KeyEventListener mainDisplayHandler, Supplier<String> lastCalledNumberSupplier, 285 @Nullable ComponentName customInputServiceComponent, 286 IntSupplier longPressDelaySupplier) { 287 mContext = context; 288 mInputHalService = inputHalService; 289 mTelecomManager = telecomManager; 290 mAssistUtils = assistUtils; 291 mMainDisplayHandler = mainDisplayHandler; 292 mLastCalledNumberSupplier = lastCalledNumberSupplier; 293 mCustomInputServiceComponent = customInputServiceComponent; 294 mLongPressDelaySupplier = longPressDelaySupplier; 295 296 mVoiceKeyTimer = 297 new KeyPressTimer( 298 handler, longPressDelaySupplier, this::handleVoiceAssistLongPress); 299 mCallKeyTimer = 300 new KeyPressTimer(handler, longPressDelaySupplier, this::handleCallLongPress); 301 } 302 303 @VisibleForTesting setHandledKeys(InputFilter[] handledKeys)304 synchronized void setHandledKeys(InputFilter[] handledKeys) { 305 mHandledKeys.clear(); 306 for (InputFilter handledKey : handledKeys) { 307 mHandledKeys.put(handledKey.mTargetDisplay, handledKey.mKeyCode); 308 } 309 } 310 311 /** 312 * Set projection key event listener. If null, unregister listener. 313 */ setProjectionKeyEventHandler( @ullable CarProjectionManager.ProjectionKeyEventHandler listener, @Nullable BitSet events)314 public void setProjectionKeyEventHandler( 315 @Nullable CarProjectionManager.ProjectionKeyEventHandler listener, 316 @Nullable BitSet events) { 317 synchronized (this) { 318 mProjectionKeyEventHandler = listener; 319 mProjectionKeyEventsSubscribed.clear(); 320 if (events != null) { 321 mProjectionKeyEventsSubscribed.or(events); 322 } 323 } 324 } 325 setInstrumentClusterKeyListener(KeyEventListener listener)326 public void setInstrumentClusterKeyListener(KeyEventListener listener) { 327 synchronized (this) { 328 mInstrumentClusterKeyListener = listener; 329 } 330 } 331 332 @Override init()333 public void init() { 334 if (!mInputHalService.isKeyInputSupported()) { 335 Log.w(CarLog.TAG_INPUT, "Hal does not support key input."); 336 return; 337 } else if (DBG) { 338 Log.d(CarLog.TAG_INPUT, "Hal supports key input."); 339 } 340 341 342 mInputHalService.setInputListener(this); 343 synchronized (this) { 344 mCarInputListenerBound = bindCarInputService(); 345 } 346 if (mBluetoothAdapter != null) { 347 mBluetoothAdapter.getProfileProxy( 348 mContext, mBluetoothProfileServiceListener, BluetoothProfile.HEADSET_CLIENT); 349 } 350 } 351 352 @Override release()353 public void release() { 354 synchronized (this) { 355 mProjectionKeyEventHandler = null; 356 mProjectionKeyEventsSubscribed.clear(); 357 mInstrumentClusterKeyListener = null; 358 if (mCarInputListenerBound) { 359 mContext.unbindService(mInputServiceConnection); 360 mCarInputListenerBound = false; 361 } 362 } 363 synchronized (mBluetoothProfileServiceListener) { 364 if (mBluetoothHeadsetClient != null) { 365 mBluetoothAdapter.closeProfileProxy( 366 BluetoothProfile.HEADSET_CLIENT, mBluetoothHeadsetClient); 367 mBluetoothHeadsetClient = null; 368 } 369 } 370 } 371 372 @Override onKeyEvent(KeyEvent event, int targetDisplay)373 public void onKeyEvent(KeyEvent event, int targetDisplay) { 374 // Give a car specific input listener the opportunity to intercept any input from the car 375 ICarInputListener carInputListener; 376 synchronized (this) { 377 carInputListener = mCarInputListener; 378 } 379 if (carInputListener != null && isCustomEventHandler(event, targetDisplay)) { 380 try { 381 carInputListener.onKeyEvent(event, targetDisplay); 382 } catch (RemoteException e) { 383 Log.e(CarLog.TAG_INPUT, "Error while calling car input service", e); 384 } 385 // Custom input service handled the event, nothing more to do here. 386 return; 387 } 388 389 // Special case key code that have special "long press" handling for automotive 390 switch (event.getKeyCode()) { 391 case KeyEvent.KEYCODE_VOICE_ASSIST: 392 handleVoiceAssistKey(event); 393 return; 394 case KeyEvent.KEYCODE_CALL: 395 handleCallKey(event); 396 return; 397 default: 398 break; 399 } 400 401 // Allow specifically targeted keys to be routed to the cluster 402 if (targetDisplay == InputHalService.DISPLAY_INSTRUMENT_CLUSTER) { 403 handleInstrumentClusterKey(event); 404 } else { 405 mMainDisplayHandler.onKeyEvent(event); 406 } 407 } 408 isCustomEventHandler(KeyEvent event, int targetDisplay)409 private synchronized boolean isCustomEventHandler(KeyEvent event, int targetDisplay) { 410 return mHandledKeys.containsEntry(targetDisplay, event.getKeyCode()); 411 } 412 handleVoiceAssistKey(KeyEvent event)413 private void handleVoiceAssistKey(KeyEvent event) { 414 int action = event.getAction(); 415 if (action == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 416 mVoiceKeyTimer.keyDown(); 417 dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_KEY_DOWN); 418 } else if (action == KeyEvent.ACTION_UP) { 419 if (mVoiceKeyTimer.keyUp()) { 420 // Long press already handled by handleVoiceAssistLongPress(), nothing more to do. 421 // Hand it off to projection, if it's interested, otherwise we're done. 422 dispatchProjectionKeyEvent( 423 CarProjectionManager.KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP); 424 return; 425 } 426 427 if (dispatchProjectionKeyEvent( 428 CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP)) { 429 return; 430 } 431 432 launchDefaultVoiceAssistantHandler(); 433 } 434 } 435 handleVoiceAssistLongPress()436 private void handleVoiceAssistLongPress() { 437 // If projection wants this event, let it take it. 438 if (dispatchProjectionKeyEvent( 439 CarProjectionManager.KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN)) { 440 return; 441 } 442 // Otherwise, try to launch voice recognition on a BT device. 443 if (launchBluetoothVoiceRecognition()) { 444 return; 445 } 446 // Finally, fallback to the default voice assist handling. 447 launchDefaultVoiceAssistantHandler(); 448 } 449 handleCallKey(KeyEvent event)450 private void handleCallKey(KeyEvent event) { 451 int action = event.getAction(); 452 if (action == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 453 mCallKeyTimer.keyDown(); 454 dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN); 455 } else if (action == KeyEvent.ACTION_UP) { 456 if (mCallKeyTimer.keyUp()) { 457 // Long press already handled by handleCallLongPress(), nothing more to do. 458 // Hand it off to projection, if it's interested, otherwise we're done. 459 dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_UP); 460 return; 461 } 462 463 if (acceptCallIfRinging()) { 464 // Ringing call answered, nothing more to do. 465 return; 466 } 467 468 if (dispatchProjectionKeyEvent( 469 CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP)) { 470 return; 471 } 472 473 launchDialerHandler(); 474 } 475 } 476 handleCallLongPress()477 private void handleCallLongPress() { 478 // Long-press answers call if ringing, same as short-press. 479 if (acceptCallIfRinging()) { 480 return; 481 } 482 483 if (dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN)) { 484 return; 485 } 486 487 dialLastCallHandler(); 488 } 489 dispatchProjectionKeyEvent(@arProjectionManager.KeyEventNum int event)490 private boolean dispatchProjectionKeyEvent(@CarProjectionManager.KeyEventNum int event) { 491 CarProjectionManager.ProjectionKeyEventHandler projectionKeyEventHandler; 492 synchronized (this) { 493 projectionKeyEventHandler = mProjectionKeyEventHandler; 494 if (projectionKeyEventHandler == null || !mProjectionKeyEventsSubscribed.get(event)) { 495 // No event handler, or event handler doesn't want this event - we're done. 496 return false; 497 } 498 } 499 500 projectionKeyEventHandler.onKeyEvent(event); 501 return true; 502 } 503 launchDialerHandler()504 private void launchDialerHandler() { 505 Log.i(CarLog.TAG_INPUT, "call key, launch dialer intent"); 506 Intent dialerIntent = new Intent(Intent.ACTION_DIAL); 507 mContext.startActivityAsUser(dialerIntent, null, UserHandle.CURRENT_OR_SELF); 508 } 509 dialLastCallHandler()510 private void dialLastCallHandler() { 511 Log.i(CarLog.TAG_INPUT, "call key, dialing last call"); 512 513 String lastNumber = mLastCalledNumberSupplier.get(); 514 if (!TextUtils.isEmpty(lastNumber)) { 515 Intent callLastNumberIntent = new Intent(Intent.ACTION_CALL) 516 .setData(Uri.fromParts("tel", lastNumber, null)) 517 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 518 mContext.startActivityAsUser(callLastNumberIntent, null, UserHandle.CURRENT_OR_SELF); 519 } 520 } 521 acceptCallIfRinging()522 private boolean acceptCallIfRinging() { 523 if (mTelecomManager != null && mTelecomManager.isRinging()) { 524 Log.i(CarLog.TAG_INPUT, "call key while ringing. Answer the call!"); 525 mTelecomManager.acceptRingingCall(); 526 return true; 527 } 528 529 return false; 530 } 531 launchBluetoothVoiceRecognition()532 private boolean launchBluetoothVoiceRecognition() { 533 synchronized (mBluetoothProfileServiceListener) { 534 if (mBluetoothHeadsetClient == null) { 535 return false; 536 } 537 // getConnectedDevices() does not make any guarantees about the order of the returned 538 // list. As of 2019-02-26, this code is only triggered through a long-press of the 539 // voice recognition key, so handling of multiple connected devices that support voice 540 // recognition is not expected to be a primary use case. 541 List<BluetoothDevice> devices = mBluetoothHeadsetClient.getConnectedDevices(); 542 if (devices != null) { 543 for (BluetoothDevice device : devices) { 544 Bundle bundle = mBluetoothHeadsetClient.getCurrentAgFeatures(device); 545 if (bundle == null || !bundle.getBoolean( 546 BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION)) { 547 continue; 548 } 549 if (mBluetoothHeadsetClient.startVoiceRecognition(device)) { 550 Log.d(CarLog.TAG_INPUT, "started voice recognition on BT device at " 551 + device.getAddress()); 552 return true; 553 } 554 } 555 } 556 } 557 return false; 558 } 559 launchDefaultVoiceAssistantHandler()560 private void launchDefaultVoiceAssistantHandler() { 561 Log.i(CarLog.TAG_INPUT, "voice key, invoke AssistUtils"); 562 563 if (mAssistUtils.getAssistComponentForUser(ActivityManager.getCurrentUser()) == null) { 564 Log.w(CarLog.TAG_INPUT, "Unable to retrieve assist component for current user"); 565 return; 566 } 567 568 final Bundle args = new Bundle(); 569 args.putBoolean(EXTRA_CAR_PUSH_TO_TALK, true); 570 571 mAssistUtils.showSessionForActiveService(args, 572 SHOW_SOURCE_PUSH_TO_TALK, mShowCallback, null /*activityToken*/); 573 } 574 handleInstrumentClusterKey(KeyEvent event)575 private void handleInstrumentClusterKey(KeyEvent event) { 576 KeyEventListener listener = null; 577 synchronized (this) { 578 listener = mInstrumentClusterKeyListener; 579 } 580 if (listener == null) { 581 return; 582 } 583 listener.onKeyEvent(event); 584 } 585 586 @Override dump(PrintWriter writer)587 public synchronized void dump(PrintWriter writer) { 588 writer.println("*Input Service*"); 589 writer.println("mCustomInputServiceComponent: " + mCustomInputServiceComponent); 590 writer.println("mCarInputListenerBound: " + mCarInputListenerBound); 591 writer.println("mCarInputListener: " + mCarInputListener); 592 writer.println("Long-press delay: " + mLongPressDelaySupplier.getAsInt() + "ms"); 593 } 594 bindCarInputService()595 private boolean bindCarInputService() { 596 if (mCustomInputServiceComponent == null) { 597 Log.i(CarLog.TAG_INPUT, "Custom input service was not configured"); 598 return false; 599 } 600 601 Log.d(CarLog.TAG_INPUT, "bindCarInputService, component: " + mCustomInputServiceComponent); 602 603 Intent intent = new Intent(); 604 Bundle extras = new Bundle(); 605 extras.putBinder(CarInputHandlingService.INPUT_CALLBACK_BINDER_KEY, mCallback); 606 intent.putExtras(extras); 607 intent.setComponent(mCustomInputServiceComponent); 608 return mContext.bindService(intent, mInputServiceConnection, Context.BIND_AUTO_CREATE); 609 } 610 } 611