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