| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.android.car.kitchensink.input; |
| |
| import static android.hardware.automotive.vehicle.V2_0.SubscribeFlags.EVENTS_FROM_ANDROID; |
| import static android.hardware.automotive.vehicle.V2_0.SubscribeFlags.EVENTS_FROM_CAR; |
| import static android.hardware.automotive.vehicle.V2_0.VehicleDisplay.INSTRUMENT_CLUSTER; |
| import static android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction.ACTION_DOWN; |
| import static android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction.ACTION_UP; |
| import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_KEY_INPUT; |
| |
| import android.annotation.Nullable; |
| import android.annotation.StringRes; |
| import android.hardware.automotive.vehicle.V2_0.IVehicle; |
| import android.hardware.automotive.vehicle.V2_0.IVehicleCallback; |
| import android.hardware.automotive.vehicle.V2_0.IVehicleCallback.Stub; |
| import android.hardware.automotive.vehicle.V2_0.SubscribeOptions; |
| import android.hardware.automotive.vehicle.V2_0.VehicleArea; |
| import android.hardware.automotive.vehicle.V2_0.VehicleDisplay; |
| import android.hardware.automotive.vehicle.V2_0.VehiclePropValue; |
| import android.hardware.automotive.vehicle.V2_0.VehiclePropertyGroup; |
| import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus; |
| import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.text.method.ScrollingMovementMethod; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.Button; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import androidx.fragment.app.Fragment; |
| |
| import com.android.car.keventreader.EventReaderService; |
| import com.android.car.keventreader.IEventCallback; |
| import com.android.car.keventreader.KeypressEvent; |
| |
| import com.google.android.car.kitchensink.R; |
| import com.google.android.collect.Lists; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.function.Function; |
| |
| /** |
| * Test input event handling to system. |
| * vehicle hal should have VEHICLE_PROPERTY_HW_KEY_INPUT support for this to work. |
| */ |
| public class InputTestFragment extends Fragment { |
| |
| private static final String TAG = "CAR.INPUT.KS"; |
| |
| private static final Button BREAK_LINE = null; |
| |
| private final List<View> mButtons = new ArrayList<>(); |
| |
| // This is fake data generation property available only in emulated VHAL implementation. |
| private static final int sGenerateFakeDataControllingProperty = |
| 0x0666 | VehiclePropertyGroup.VENDOR | VehicleArea.GLOBAL | VehiclePropertyType.MIXED; |
| // The key press command is sent with the fake data generation property. It's matching the |
| // command defined in the emulated VHAL implementation. |
| private static final int sKeyPressCommand = 100; |
| |
| private IVehicle mVehicle; |
| |
| // Helper class for counting the number of key down vs. key up events received from a single |
| // source. |
| private class KeyDownCounter { |
| KeyDownCounter(Function<Integer, String> keyCodeToStringFunc) { |
| mKeyCodeToStringFunc = keyCodeToStringFunc; |
| } |
| |
| public synchronized void count(int keyCode, boolean keyDown) { |
| Integer count = mKeyCodeToCountMap.get(keyCode); |
| if (count == null) { |
| count = 0; |
| } |
| count += keyDown ? 1 : -1; |
| if (count == 0) { |
| mKeyCodeToCountMap.remove(keyCode); |
| } else { |
| mKeyCodeToCountMap.put(keyCode, count); |
| } |
| mMainHandler.post(mRefreshKeysDownTextViewRunnable); |
| } |
| |
| public synchronized String toString() { |
| StringBuilder sb = new StringBuilder(); |
| for (Map.Entry<Integer, Integer> entry : mKeyCodeToCountMap.entrySet()) { |
| sb.append(" ").append(mKeyCodeToStringFunc.apply(entry.getKey())); |
| if (entry.getValue() != 1) { |
| sb.append("x").append(entry.getValue()); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| // Function to translate keycodes into strings for this source. |
| private final Function<Integer, String> mKeyCodeToStringFunc; |
| |
| // LinkedHashMap used for deterministic iteration order. |
| private final Map<Integer, Integer> mKeyCodeToCountMap = new LinkedHashMap<>(); |
| } |
| |
| // KeyDownCounters for events from EventReaderService and VHAL. Note that they use different |
| // key code mappings. |
| private final KeyDownCounter mEventReaderServiceKeyDownCounter = |
| new KeyDownCounter(k -> KeypressEvent.keycodeToString(k)); |
| private final KeyDownCounter mVhalKeyDownCounter = |
| new KeyDownCounter(k -> KeyEvent.keyCodeToString(k)); |
| |
| private final Handler mMainHandler = new Handler(Looper.getMainLooper()); |
| |
| private TextView mKeysDownTextView; |
| |
| // Runnable posted to mMainHandler by KeyDownCounter to refresh mKeysDownTextView's text. |
| private final Runnable mRefreshKeysDownTextViewRunnable = new Runnable() { |
| @Override |
| public void run() { |
| StringBuilder sb = new StringBuilder("VHAL keys down:"); |
| sb.append(mVhalKeyDownCounter.toString()); |
| |
| if (mEventReaderService == null) { |
| sb.append("\nCould not connect to EventReaderService. Is keventreader running?"); |
| } else { |
| sb.append("\nEventReaderService keys down:"); |
| sb.append(mEventReaderServiceKeyDownCounter.toString()); |
| } |
| |
| mKeysDownTextView.setText(sb.toString()); |
| } |
| }; |
| |
| private EventReaderService mEventReaderService; |
| |
| private final IEventCallback.Stub mKeypressEventHandler = new IEventCallback.Stub() { |
| private String prettyPrint(KeypressEvent event) { |
| return String.format("Event{source = %s, keycode = %s, key%s}\n", |
| event.source, |
| event.keycodeToString(), |
| event.isKeydown ? "down" : "up"); |
| } |
| |
| @Override |
| public void onEvent(KeypressEvent keypressEvent) throws RemoteException { |
| Log.d(TAG, "received event " + keypressEvent); |
| mEventReaderServiceKeyDownCounter.count(keypressEvent.keycode, keypressEvent.isKeydown); |
| } |
| }; |
| |
| private final IVehicleCallback.Stub mHalKeyEventHandler = new Stub() { |
| private String prettyPrint(VehiclePropValue event) { |
| if (event.prop != HW_KEY_INPUT) return ""; |
| if (event.value == null || |
| event.value.int32Values == null || |
| event.value.int32Values.size() < 2) return ""; |
| return String.format("Event{source = HAL, keycode = %s, key%s}\n", |
| event.value.int32Values.get(1), |
| event.value.int32Values.get(0) == ACTION_DOWN ? "down" : "up"); |
| } |
| |
| @Override |
| public void onPropertyEvent(ArrayList<VehiclePropValue> propValues) throws RemoteException { |
| for (VehiclePropValue vpv : propValues) { |
| Log.d(TAG, "received event " + prettyPrint(vpv)); |
| if (vpv.prop != HW_KEY_INPUT || vpv.value == null || vpv.value.int32Values == null |
| || vpv.value.int32Values.size() < 2) { |
| continue; |
| } |
| int keycode = vpv.value.int32Values.get(1); |
| switch (vpv.value.int32Values.get(0)) { |
| case ACTION_DOWN: |
| mVhalKeyDownCounter.count(keycode, true); |
| break; |
| case ACTION_UP: |
| mVhalKeyDownCounter.count(keycode, false); |
| break; |
| default: |
| Log.e(TAG, "Unrecognized VehicleHwKeyInputAction: " |
| + vpv.value.int32Values.get(0)); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void onPropertySet(VehiclePropValue propValue) {} |
| |
| @Override |
| public void onPropertySetError(int errorCode, int propId, int areaId) {} |
| }; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| try { |
| mVehicle = IVehicle.getService(); |
| } catch (NoSuchElementException ex) { |
| throw new RuntimeException("Couldn't connect to " + IVehicle.kInterfaceName, ex); |
| } catch (RemoteException e) { |
| throw new RuntimeException("Failed to connect to IVehicle"); |
| } |
| Log.d(TAG, "Connected to IVehicle service: " + mVehicle); |
| |
| mEventReaderService = EventReaderService.tryGet(); |
| Log.d(TAG, "Key Event Reader service: " + mEventReaderService); |
| if (mEventReaderService != null) { |
| mEventReaderService.registerCallback(mKeypressEventHandler); |
| } |
| |
| SubscribeOptions subscribeOption = new SubscribeOptions(); |
| subscribeOption.propId = HW_KEY_INPUT; |
| subscribeOption.flags = EVENTS_FROM_CAR | EVENTS_FROM_ANDROID; |
| ArrayList<SubscribeOptions> subscribeOptions = new ArrayList<>(); |
| subscribeOptions.add(subscribeOption); |
| try { |
| mVehicle.subscribe(mHalKeyEventHandler, subscribeOptions); |
| } catch (RemoteException e) { |
| Log.e(TAG, "failed to connect to VHAL for key events", e); |
| } |
| } |
| |
| @Nullable |
| @Override |
| public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, |
| @Nullable Bundle savedInstanceState) { |
| View view = inflater.inflate(R.layout.input_test, container, false); |
| |
| mKeysDownTextView = view.findViewById(R.id.events_list); |
| mKeysDownTextView.setMovementMethod(new ScrollingMovementMethod()); |
| mRefreshKeysDownTextViewRunnable.run(); |
| |
| TextView steeringWheelLabel = new TextView(getActivity() /*context*/); |
| steeringWheelLabel.setText(R.string.steering_wheel); |
| steeringWheelLabel.setTextSize(getResources().getDimension(R.dimen.car_title2_size)); |
| |
| Collections.addAll(mButtons, |
| BREAK_LINE, |
| createButton(R.string.home, KeyEvent.KEYCODE_HOME), |
| createButton(R.string.volume_up, KeyEvent.KEYCODE_VOLUME_UP), |
| createButton(R.string.volume_down, KeyEvent.KEYCODE_VOLUME_DOWN), |
| createButton(R.string.volume_mute, KeyEvent.KEYCODE_VOLUME_MUTE), |
| createButton(R.string.voice, KeyEvent.KEYCODE_VOICE_ASSIST), |
| BREAK_LINE, |
| createButton(R.string.music, KeyEvent.KEYCODE_MUSIC), |
| createButton(R.string.music_play, KeyEvent.KEYCODE_MEDIA_PLAY), |
| createButton(R.string.music_stop, KeyEvent.KEYCODE_MEDIA_STOP), |
| createButton(R.string.next_song, KeyEvent.KEYCODE_MEDIA_NEXT), |
| createButton(R.string.prev_song, KeyEvent.KEYCODE_MEDIA_PREVIOUS), |
| createButton(R.string.tune_right, KeyEvent.KEYCODE_CHANNEL_UP), |
| createButton(R.string.tune_left, KeyEvent.KEYCODE_CHANNEL_DOWN), |
| BREAK_LINE, |
| createButton(R.string.call_send, KeyEvent.KEYCODE_CALL), |
| createButton(R.string.call_end, KeyEvent.KEYCODE_ENDCALL), |
| BREAK_LINE, |
| steeringWheelLabel, |
| BREAK_LINE, |
| createButton(R.string.sw_left, KeyEvent.KEYCODE_DPAD_LEFT, INSTRUMENT_CLUSTER), |
| createButton(R.string.sw_right, KeyEvent.KEYCODE_DPAD_RIGHT, |
| INSTRUMENT_CLUSTER), |
| createButton(R.string.sw_up, KeyEvent.KEYCODE_DPAD_UP, INSTRUMENT_CLUSTER), |
| createButton(R.string.sw_down, KeyEvent.KEYCODE_DPAD_DOWN, INSTRUMENT_CLUSTER), |
| createButton(R.string.sw_center, KeyEvent.KEYCODE_DPAD_CENTER, |
| INSTRUMENT_CLUSTER), |
| createButton(R.string.sw_back, KeyEvent.KEYCODE_BACK, INSTRUMENT_CLUSTER) |
| ); |
| |
| addButtonsToPanel(view.findViewById(R.id.input_buttons), mButtons); |
| |
| return view; |
| } |
| |
| private Button createButton(@StringRes int textResId, int keyCode) { |
| return createButton(textResId, keyCode, VehicleDisplay.MAIN); |
| } |
| |
| private Button createButton(@StringRes int textResId, int keyCode, int targetDisplay) { |
| Button button = new Button(getContext()); |
| button.setText(getContext().getString(textResId)); |
| button.setTextSize(getResources().getDimension(R.dimen.car_button_text_size)); |
| button.setOnClickListener(v -> onButtonClick(keyCode, targetDisplay)); |
| return button; |
| } |
| |
| private void onButtonClick(int keyCode, int targetDisplay) { |
| VehiclePropValue prop = new VehiclePropValue(); |
| prop.prop = sGenerateFakeDataControllingProperty; |
| prop.value.int32Values.addAll(Lists.newArrayList( |
| sKeyPressCommand, HW_KEY_INPUT, keyCode, targetDisplay)); |
| int status; |
| try { |
| status = mVehicle.set(prop); |
| } catch (RemoteException e) { |
| throw new RuntimeException("Failed to inject key press"); |
| } |
| |
| if (VehiclePropertyStatus.AVAILABLE != status) { |
| Toast.makeText(getContext(), "Failed to inject key event, status:" + status, |
| Toast.LENGTH_LONG).show(); |
| } |
| } |
| |
| @Override |
| public void onDestroyView() { |
| super.onDestroyView(); |
| mButtons.clear(); |
| if (mEventReaderService != null) { |
| mEventReaderService.unregisterCallback(mKeypressEventHandler); |
| } |
| try { |
| mVehicle.unsubscribe(mHalKeyEventHandler, HW_KEY_INPUT); |
| } catch (RemoteException e) { |
| Log.e(TAG, "failed to remove HAL registration for keypress events", e); |
| } |
| } |
| |
| private void addButtonsToPanel(LinearLayout root, List<View> buttons) { |
| LinearLayout panel = null; |
| for (View button : buttons) { |
| if (button == BREAK_LINE || panel == null) { |
| panel = new LinearLayout(getContext()); |
| panel.setOrientation(LinearLayout.HORIZONTAL); |
| root.addView(panel); |
| } else { |
| panel.addView(button); |
| panel.setPadding(0, 10, 10, 0); |
| } |
| } |
| } |
| } |