• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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