Merge "Adding the workflow for HW_CUSTOM_INPUT in Car Framework"
diff --git a/car-lib/src/android/car/input/CarInputManager.java b/car-lib/src/android/car/input/CarInputManager.java
index dbbf18c..ebcc193 100644
--- a/car-lib/src/android/car/input/CarInputManager.java
+++ b/car-lib/src/android/car/input/CarInputManager.java
@@ -23,6 +23,7 @@
import android.car.CarManagerBase;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Slog;
import android.util.SparseArray;
import android.view.KeyEvent;
@@ -42,6 +43,10 @@
*/
public final class CarInputManager extends CarManagerBase {
+ private static final String TAG = CarInputManager.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
/**
* Callback for capturing input events.
*/
@@ -49,20 +54,27 @@
/**
* Key events were captured.
*/
- void onKeyEvents(int targetDisplayId, @NonNull List<KeyEvent> keyEvents);
+ // TODO(b/164195589): Rename targetDisplayId parameter to targetDisplayType
+ default void onKeyEvents(int targetDisplayId, @NonNull List<KeyEvent> keyEvents) {}
/**
* Rotary events were captured.
*/
- void onRotaryEvents(int targetDisplayId, @NonNull List<RotaryEvent> events);
+ default void onRotaryEvents(int targetDisplayId, @NonNull List<RotaryEvent> events) {}
/**
* Capture state for the display has changed due to other client making requests or
* releasing capture. Client should check {@code activeInputTypes} for which input types
* are currently captured.
*/
- void onCaptureStateChanged(int targetDisplayId,
- @NonNull @InputTypeEnum int[] activeInputTypes);
+ default void onCaptureStateChanged(int targetDisplayId,
+ @NonNull @InputTypeEnum int[] activeInputTypes) {}
+
+ /**
+ * Custom input events were captured.
+ */
+ default void onCustomInputEvents(int targetDisplayId,
+ @NonNull List<CustomInputEvent> events) {}
}
/**
@@ -137,15 +149,20 @@
public static final int INPUT_TYPE_DPAD_KEYS = 100;
/**
- * This is for all KEYCODE_NAVIGATE_* keys.
+ * This is for all {@code KeyEvent#KEYCODE_NAVIGATE_*} keys.
*/
public static final int INPUT_TYPE_NAVIGATE_KEYS = 101;
/**
- * This is for all KEYCODE_SYSTEM_NAVIGATE_* keys.
+ * This is for all {@code KeyEvent#KEYCODE_SYSTEM_NAVIGATE_*} keys.
*/
public static final int INPUT_TYPE_SYSTEM_NAVIGATE_KEYS = 102;
+ /**
+ * This is for {@code HW_CUSTOM_INPUT} events.
+ */
+ public static final int INPUT_TYPE_CUSTOM_INPUT_EVENT = 200;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "INPUT_TYPE_", value = {
@@ -154,7 +171,8 @@
INPUT_TYPE_ROTARY_VOLUME,
INPUT_TYPE_DPAD_KEYS,
INPUT_TYPE_NAVIGATE_KEYS,
- INPUT_TYPE_SYSTEM_NAVIGATE_KEYS
+ INPUT_TYPE_SYSTEM_NAVIGATE_KEYS,
+ INPUT_TYPE_CUSTOM_INPUT_EVENT,
})
@Target({ElementType.TYPE_USE})
public @interface InputTypeEnum {}
@@ -299,6 +317,18 @@
});
}
+ private void dispatchCustomInputEvents(int targetDisplayType, List<CustomInputEvent> events) {
+ getEventHandler().post(() -> {
+ CarInputCaptureCallback callback = getCallback(targetDisplayType);
+ if (DEBUG) {
+ Slog.d(TAG, "Firing events " + events + " on callback " + callback);
+ }
+ if (callback != null) {
+ callback.onCustomInputEvents(targetDisplayType, events);
+ }
+ });
+ }
+
private static final class ICarInputCallbackImpl extends ICarInputCallback.Stub {
private final WeakReference<CarInputManager> mManager;
@@ -333,5 +363,14 @@
}
manager.dispatchOnCaptureStateChanged(targetDisplayType, activeInputTypes);
}
+
+ @Override
+ public void onCustomInputEvents(int targetDisplayId, List<CustomInputEvent> events) {
+ CarInputManager manager = mManager.get();
+ if (manager == null) {
+ return;
+ }
+ manager.dispatchCustomInputEvents(targetDisplayId, events);
+ }
}
}
diff --git a/car-lib/src/android/car/input/CustomInputEvent.aidl b/car-lib/src/android/car/input/CustomInputEvent.aidl
new file mode 100644
index 0000000..01bb4ec
--- /dev/null
+++ b/car-lib/src/android/car/input/CustomInputEvent.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2020 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 android.car.input;
+
+parcelable CustomInputEvent;
diff --git a/car-lib/src/android/car/input/CustomInputEvent.java b/car-lib/src/android/car/input/CustomInputEvent.java
new file mode 100644
index 0000000..129d8fa
--- /dev/null
+++ b/car-lib/src/android/car/input/CustomInputEvent.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2020 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 android.car.input;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * {@code Parcelable} containing custom input event.
+ *
+ * <p>A custom input event representing HW_CUSTOM_INPUT event defined in
+ * {@code hardware/interfaces/automotive/vehicle/2.0/types.hal}.
+ *
+ * @hide
+ */
+// Note: When re-generating code, make sure inputCodeToString raises an exception in case of invalid
+// input.
+// TODO(b/12219669): Check with INPUT_CODE_Fn constants should move to
+// android/car/Constants/CommonConstants.java. If keeping these constants, than add unit tests.
+@DataClass(
+ genEqualsHashCode = true,
+ genAidl = true)
+public final class CustomInputEvent implements Parcelable {
+
+ // The following constant values must be in sync with the ones defined in
+ // {@code hardware/interfaces/automotive/vehicle/2.0/types.hal}
+ public static final int INPUT_CODE_F1 = 1001;
+ public static final int INPUT_CODE_F2 = 1002;
+ public static final int INPUT_CODE_F3 = 1003;
+ public static final int INPUT_CODE_F4 = 1004;
+ public static final int INPUT_CODE_F5 = 1005;
+ public static final int INPUT_CODE_F6 = 1006;
+ public static final int INPUT_CODE_F7 = 1007;
+ public static final int INPUT_CODE_F8 = 1008;
+ public static final int INPUT_CODE_F9 = 1009;
+ public static final int INPUT_CODE_F10 = 1010;
+
+ @InputCode
+ private final int mInputCode;
+
+ private final int mTargetDisplayType;
+ private final int mRepeatCounter;
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen --to-string $ANDROID_BUILD_TOP/packages/services/Car/car-lib/src/android/car
+ // /input/CustomInputEvent.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @android.annotation.IntDef(prefix = "INPUT_CODE_", value = {
+ INPUT_CODE_F1,
+ INPUT_CODE_F2,
+ INPUT_CODE_F3,
+ INPUT_CODE_F4,
+ INPUT_CODE_F5,
+ INPUT_CODE_F6,
+ INPUT_CODE_F7,
+ INPUT_CODE_F8,
+ INPUT_CODE_F9,
+ INPUT_CODE_F10
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface InputCode {
+ }
+
+ @DataClass.Generated.Member
+ public static String inputCodeToString(@InputCode int value) {
+ switch (value) {
+ case INPUT_CODE_F1:
+ return "INPUT_CODE_F1";
+ case INPUT_CODE_F2:
+ return "INPUT_CODE_F2";
+ case INPUT_CODE_F3:
+ return "INPUT_CODE_F3";
+ case INPUT_CODE_F4:
+ return "INPUT_CODE_F4";
+ case INPUT_CODE_F5:
+ return "INPUT_CODE_F5";
+ case INPUT_CODE_F6:
+ return "INPUT_CODE_F6";
+ case INPUT_CODE_F7:
+ return "INPUT_CODE_F7";
+ case INPUT_CODE_F8:
+ return "INPUT_CODE_F8";
+ case INPUT_CODE_F9:
+ return "INPUT_CODE_F9";
+ case INPUT_CODE_F10:
+ return "INPUT_CODE_F10";
+ default:
+ throw new java.lang.IllegalArgumentException(
+ "Invalid inputCode {" + value + "}");
+ }
+ }
+
+ @DataClass.Generated.Member
+ public CustomInputEvent(
+ @InputCode int inputCode,
+ int targetDisplayType,
+ int repeatCounter) {
+ this.mInputCode = inputCode;
+
+ if (!(mInputCode == INPUT_CODE_F1)
+ && !(mInputCode == INPUT_CODE_F2)
+ && !(mInputCode == INPUT_CODE_F3)
+ && !(mInputCode == INPUT_CODE_F4)
+ && !(mInputCode == INPUT_CODE_F5)
+ && !(mInputCode == INPUT_CODE_F6)
+ && !(mInputCode == INPUT_CODE_F7)
+ && !(mInputCode == INPUT_CODE_F8)
+ && !(mInputCode == INPUT_CODE_F9)
+ && !(mInputCode == INPUT_CODE_F10)) {
+ throw new java.lang.IllegalArgumentException(
+ "inputCode was " + mInputCode + " but must be one of: "
+ + "INPUT_CODE_F1(" + INPUT_CODE_F1 + "), "
+ + "INPUT_CODE_F2(" + INPUT_CODE_F2 + "), "
+ + "INPUT_CODE_F3(" + INPUT_CODE_F3 + "), "
+ + "INPUT_CODE_F4(" + INPUT_CODE_F4 + "), "
+ + "INPUT_CODE_F5(" + INPUT_CODE_F5 + "), "
+ + "INPUT_CODE_F6(" + INPUT_CODE_F6 + "), "
+ + "INPUT_CODE_F7(" + INPUT_CODE_F7 + "), "
+ + "INPUT_CODE_F8(" + INPUT_CODE_F8 + "), "
+ + "INPUT_CODE_F9(" + INPUT_CODE_F9 + "), "
+ + "INPUT_CODE_F10(" + INPUT_CODE_F10 + ")");
+ }
+
+ this.mTargetDisplayType = targetDisplayType;
+ this.mRepeatCounter = repeatCounter;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public @InputCode
+ int getInputCode() {
+ return mInputCode;
+ }
+
+ @DataClass.Generated.Member
+ public int getTargetDisplayType() {
+ return mTargetDisplayType;
+ }
+
+ @DataClass.Generated.Member
+ public int getRepeatCounter() {
+ return mRepeatCounter;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "CustomInputEvent { " +
+ "inputCode = " + inputCodeToString(mInputCode) + ", " +
+ "targetDisplayType = " + mTargetDisplayType + ", " +
+ "repeatCounter = " + mRepeatCounter +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@android.annotation.Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(CustomInputEvent other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ CustomInputEvent that = (CustomInputEvent) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mInputCode == that.mInputCode
+ && mTargetDisplayType == that.mTargetDisplayType
+ && mRepeatCounter == that.mRepeatCounter;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + mInputCode;
+ _hash = 31 * _hash + mTargetDisplayType;
+ _hash = 31 * _hash + mRepeatCounter;
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeInt(mInputCode);
+ dest.writeInt(mTargetDisplayType);
+ dest.writeInt(mRepeatCounter);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ CustomInputEvent(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ int inputCode = in.readInt();
+ int targetDisplayType = in.readInt();
+ int repeatCounter = in.readInt();
+
+ this.mInputCode = inputCode;
+
+ if (!(mInputCode == INPUT_CODE_F1)
+ && !(mInputCode == INPUT_CODE_F2)
+ && !(mInputCode == INPUT_CODE_F3)
+ && !(mInputCode == INPUT_CODE_F4)
+ && !(mInputCode == INPUT_CODE_F5)
+ && !(mInputCode == INPUT_CODE_F6)
+ && !(mInputCode == INPUT_CODE_F7)
+ && !(mInputCode == INPUT_CODE_F8)
+ && !(mInputCode == INPUT_CODE_F9)
+ && !(mInputCode == INPUT_CODE_F10)) {
+ throw new java.lang.IllegalArgumentException(
+ "inputCode was " + mInputCode + " but must be one of: "
+ + "INPUT_CODE_F1(" + INPUT_CODE_F1 + "), "
+ + "INPUT_CODE_F2(" + INPUT_CODE_F2 + "), "
+ + "INPUT_CODE_F3(" + INPUT_CODE_F3 + "), "
+ + "INPUT_CODE_F4(" + INPUT_CODE_F4 + "), "
+ + "INPUT_CODE_F5(" + INPUT_CODE_F5 + "), "
+ + "INPUT_CODE_F6(" + INPUT_CODE_F6 + "), "
+ + "INPUT_CODE_F7(" + INPUT_CODE_F7 + "), "
+ + "INPUT_CODE_F8(" + INPUT_CODE_F8 + "), "
+ + "INPUT_CODE_F9(" + INPUT_CODE_F9 + "), "
+ + "INPUT_CODE_F10(" + INPUT_CODE_F10 + ")");
+ }
+
+ this.mTargetDisplayType = targetDisplayType;
+ this.mRepeatCounter = repeatCounter;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull
+ Parcelable.Creator<CustomInputEvent> CREATOR
+ = new Parcelable.Creator<CustomInputEvent>() {
+ @Override
+ public CustomInputEvent[] newArray(int size) {
+ return new CustomInputEvent[size];
+ }
+
+ @Override
+ public CustomInputEvent createFromParcel(@NonNull Parcel in) {
+ return new CustomInputEvent(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1600715769152L,
+ codegenVersion = "1.0.15",
+ sourceFile = "packages/services/Car/car-lib/src/android/car/input/CustomInputEvent"
+ + ".java",
+ inputSignatures = "public static final int INPUT_CODE_F1\npublic static final int "
+ + "INPUT_CODE_F2\npublic static final int INPUT_CODE_F3\npublic static final"
+ + " int INPUT_CODE_F4\npublic static final int INPUT_CODE_F5\npublic static"
+ + " final int INPUT_CODE_F6\npublic static final int INPUT_CODE_F7\npublic "
+ + "static final int INPUT_CODE_F8\npublic static final int "
+ + "INPUT_CODE_F9\npublic static final int INPUT_CODE_F10\nprivate final "
+ + "@android.car.input.CustomInputEvent.InputCode int mInputCode\nprivate "
+ + "final int mTargetDisplayType\nprivate final int mRepeatCounter\nclass "
+ + "CustomInputEvent extends java.lang.Object implements [android.os"
+ + ".Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true,"
+ + " genAidl=true)")
+ @Deprecated
+ private void __metadata() {
+ }
+
+ //@formatter:on
+ // End of generated code
+}
diff --git a/car-lib/src/android/car/input/ICarInputCallback.aidl b/car-lib/src/android/car/input/ICarInputCallback.aidl
index 218ff2c..afe5cec 100644
--- a/car-lib/src/android/car/input/ICarInputCallback.aidl
+++ b/car-lib/src/android/car/input/ICarInputCallback.aidl
@@ -15,6 +15,7 @@
*/
package android.car.input;
+import android.car.input.CustomInputEvent;
import android.car.input.RotaryEvent;
import android.view.KeyEvent;
@@ -27,4 +28,5 @@
void onKeyEvents(int targetDisplayType, in List<KeyEvent> keyEvents) = 1;
void onRotaryEvents(int targetDisplayType, in List<RotaryEvent> events) = 2;
void onCaptureStateChanged(int targetDisplayType, in int[] activeInputTypes) = 3;
+ void onCustomInputEvents(int targetDisplayType, in List<CustomInputEvent> events) = 4;
}
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 409095a..74c1679 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -27,6 +27,7 @@
import android.bluetooth.BluetoothProfile;
import android.car.CarProjectionManager;
import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
import android.car.input.ICarInput;
import android.car.input.ICarInputCallback;
import android.car.input.RotaryEvent;
@@ -363,6 +364,17 @@
}
}
+ @Override
+ public void onCustomInputEvent(CustomInputEvent event) {
+ if (!mCaptureController.onCustomInputEvent(event)) {
+ Log.w(CarLog.TAG_INPUT, "Failed to propagate " + event);
+ return;
+ }
+ if (DBG) {
+ Log.d(CarLog.TAG_INPUT, "Succeed injecting " + event);
+ }
+ }
+
private static List<KeyEvent> rotaryEventToKeyEvents(RotaryEvent event) {
int numClicks = event.getNumberOfClicks();
int numEvents = numClicks * 2; // up / down per each click
diff --git a/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index eb2f958..f7fbf02 100644
--- a/service/src/com/android/car/CarShellCommand.java
+++ b/service/src/com/android/car/CarShellCommand.java
@@ -32,6 +32,7 @@
import android.app.UiModeManager;
import android.car.Car;
import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
import android.car.input.RotaryEvent;
import android.car.user.CarUserManager;
import android.car.user.UserCreationResult;
@@ -126,6 +127,7 @@
private static final String COMMAND_DISABLE_FEATURE = "disable-feature";
private static final String COMMAND_INJECT_KEY = "inject-key";
private static final String COMMAND_INJECT_ROTARY = "inject-rotary";
+ private static final String COMMAND_INJECT_CUSTOM_INPUT = "inject-custom-input";
private static final String COMMAND_GET_INITIAL_USER_INFO = "get-initial-user-info";
private static final String COMMAND_SILENT_MODE = "silent-mode";
private static final String COMMAND_SWITCH_USER = "switch-user";
@@ -194,8 +196,10 @@
private static final SparseArray<String> VALID_USER_AUTH_SET_VALUES;
private static final String VALID_USER_AUTH_SET_VALUES_HELP;
+ private static final ArrayMap<String, Integer> CUSTOM_INPUT_FUNCTION_ARGS;
+
static {
- VALID_USER_AUTH_TYPES = new SparseArray<String>(5);
+ VALID_USER_AUTH_TYPES = new SparseArray<>(5);
VALID_USER_AUTH_TYPES.put(KEY_FOB, UserIdentificationAssociationType.toString(KEY_FOB));
VALID_USER_AUTH_TYPES.put(CUSTOM_1, UserIdentificationAssociationType.toString(CUSTOM_1));
VALID_USER_AUTH_TYPES.put(CUSTOM_2, UserIdentificationAssociationType.toString(CUSTOM_2));
@@ -203,7 +207,7 @@
VALID_USER_AUTH_TYPES.put(CUSTOM_4, UserIdentificationAssociationType.toString(CUSTOM_4));
VALID_USER_AUTH_TYPES_HELP = getHelpString("types", VALID_USER_AUTH_TYPES);
- VALID_USER_AUTH_SET_VALUES = new SparseArray<String>(3);
+ VALID_USER_AUTH_SET_VALUES = new SparseArray<>(3);
VALID_USER_AUTH_SET_VALUES.put(ASSOCIATE_CURRENT_USER,
UserIdentificationAssociationSetValue.toString(ASSOCIATE_CURRENT_USER));
VALID_USER_AUTH_SET_VALUES.put(DISASSOCIATE_CURRENT_USER,
@@ -211,6 +215,18 @@
VALID_USER_AUTH_SET_VALUES.put(DISASSOCIATE_ALL_USERS,
UserIdentificationAssociationSetValue.toString(DISASSOCIATE_ALL_USERS));
VALID_USER_AUTH_SET_VALUES_HELP = getHelpString("values", VALID_USER_AUTH_SET_VALUES);
+
+ CUSTOM_INPUT_FUNCTION_ARGS = new ArrayMap<>(10);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f1", CustomInputEvent.INPUT_CODE_F1);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f2", CustomInputEvent.INPUT_CODE_F2);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f3", CustomInputEvent.INPUT_CODE_F3);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f4", CustomInputEvent.INPUT_CODE_F4);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f5", CustomInputEvent.INPUT_CODE_F5);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f6", CustomInputEvent.INPUT_CODE_F6);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f7", CustomInputEvent.INPUT_CODE_F7);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f8", CustomInputEvent.INPUT_CODE_F8);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f9", CustomInputEvent.INPUT_CODE_F9);
+ CUSTOM_INPUT_FUNCTION_ARGS.put("f10", CustomInputEvent.INPUT_CODE_F10);
}
@NonNull
@@ -362,7 +378,11 @@
pw.println("\t counter-clockwise. If not specified, it will be false.");
pw.println("\t delta_times_ms: a list of delta time (current time minus event time)");
pw.println("\t in descending order. If not specified, it will be 0.");
-
+ pw.println("\tinject-custom-input [-d display] [-r repeatCounter] EVENT");
+ pw.println("\t display: 0 for main, 1 for cluster. If not specified, it will be 0.");
+ pw.println("\t repeatCounter: number of times the button was hit (default value is 1)");
+ pw.println("\t EVENT: mandatory last argument. Possible values for for this flag are ");
+ pw.println("\t F1, F2, up to F10 (functions to defined by OEM partners)");
pw.printf("\t%s <REQ_TYPE> [--timeout TIMEOUT_MS]\n", COMMAND_GET_INITIAL_USER_INFO);
pw.println("\t Calls the Vehicle HAL to get the initial boot info, passing the given");
pw.println("\t REQ_TYPE (which could be either FIRST_BOOT, FIRST_BOOT_AFTER_OTA, ");
@@ -615,6 +635,12 @@
}
injectRotary(args, writer);
break;
+ case COMMAND_INJECT_CUSTOM_INPUT:
+ if (args.length < 2) {
+ return showInvalidArguments(writer);
+ }
+ injectCustomInputEvent(args, writer);
+ break;
case COMMAND_GET_INITIAL_USER_INFO:
getInitialUserInfo(args, writer);
break;
@@ -849,6 +875,47 @@
writer.println("Succeeded in injecting: " + rotaryEvent);
}
+ private void injectCustomInputEvent(String[] args, PrintWriter writer) {
+ int display = InputHalService.DISPLAY_MAIN;
+ int repeatCounter = 1;
+
+ int argIdx = 1;
+ for (; argIdx < args.length - 1; argIdx++) {
+ switch (args[argIdx]) {
+ case "-d":
+ display = Integer.parseInt(args[++argIdx]);
+ break;
+ case "-r":
+ repeatCounter = Integer.parseInt(args[++argIdx]);
+ break;
+ default:
+ writer.printf("Unrecognized argument: {%s}\n", args[argIdx]);
+ writer.println("Pass -help to see the full list of options");
+ return;
+ }
+ }
+
+ if (argIdx == args.length) {
+ writer.println("Last mandatory argument (fn) not passed.");
+ writer.println("Pass -help to see the full list of options");
+ return;
+ }
+
+ // Processing the last remaining argument (expected to be 'f1', 'f2', ..., 'f10').
+ String eventValue = args[argIdx].toLowerCase();
+ Integer inputCode = CUSTOM_INPUT_FUNCTION_ARGS.get(eventValue);
+ if (inputCode == null) {
+ writer.printf("Invalid input event value {%s}, valid values are f1, f2, ..., f10\n",
+ eventValue);
+ writer.println("Pass -help to see the full list of options");
+ return;
+ }
+
+ CustomInputEvent event = new CustomInputEvent(inputCode, display, repeatCounter);
+ mCarInputService.onCustomInputEvent(event);
+ writer.printf("Succeeded in injecting {%s}\n", event);
+ }
+
private void getInitialUserInfo(String[] args, PrintWriter writer) {
if (args.length < 2) {
writer.println("Insufficient number of args");
diff --git a/service/src/com/android/car/InputCaptureClientController.java b/service/src/com/android/car/InputCaptureClientController.java
index 8d140bd..01f0aa0 100644
--- a/service/src/com/android/car/InputCaptureClientController.java
+++ b/service/src/com/android/car/InputCaptureClientController.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
import android.car.input.ICarInputCallback;
import android.car.input.RotaryEvent;
import android.content.Context;
@@ -56,9 +57,9 @@
private static final String TAG = CarLog.TAG_INPUT;
/**
- * This table decides which input key goes into which input type. Not mapped here means it is
- * not supported for capturing. Rotary events are treated separately and this is only for
- * key events.
+ * This table decides which input key goes into which input type. Not mapped here means it is
+ * not supported for capturing. Rotary events are treated separately and this is only for
+ * key events.
*/
private static final Map<Integer, Integer> KEY_EVENT_TO_INPUT_TYPE = Map.ofEntries(
entry(KeyEvent.KEYCODE_DPAD_CENTER, CarInputManager.INPUT_TYPE_DPAD_KEYS),
@@ -89,7 +90,8 @@
CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
CarInputManager.INPUT_TYPE_DPAD_KEYS,
CarInputManager.INPUT_TYPE_NAVIGATE_KEYS,
- CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS
+ CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS,
+ CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT
);
private static final Set<Integer> VALID_ROTARY_TYPES = Set.of(
@@ -214,6 +216,10 @@
/** Accessed from dispatch thread only */
private final ArrayList<RotaryEvent> mRotaryEventDispatchScratchList = new ArrayList<>(1);
+ /** Accessed from dispatch thread only */
+ private final ArrayList<CustomInputEvent> mCustomInputEventDispatchScratchList =
+ new ArrayList<>(1);
+
@GuardedBy("mLock")
private int mNumKeyEventsDispatched;
@GuardedBy("mLock")
@@ -411,7 +417,7 @@
if (DBG_CALLS) {
Log.i(TAG, "releaseInputEventCapture callback:" + callback
- + ", display:" + targetDisplayType);
+ + ", display:" + targetDisplayType);
}
ClientsToDispatch clientsToDispatch = new ClientsToDispatch(targetDisplayType);
synchronized (mLock) {
@@ -481,9 +487,10 @@
/**
* Dispatches the given {@code KeyEvent} to a capturing client if there is one.
*
- * @param displayType Should be a display type defined in {@code CarInputManager} such as
- * {@link CarInputManager#TARGET_DISPLAY_TYPE_MAIN}.
- * @param event
+ * @param displayType the display type defined in {@code CarInputManager} such as
+ * {@link CarInputManager#TARGET_DISPLAY_TYPE_MAIN}
+ * @param event the key event to handle
+ *
* @return true if the event was consumed.
*/
public boolean onKeyEvent(int displayType, KeyEvent event) {
@@ -510,9 +517,10 @@
/**
* Dispatches the given {@code RotaryEvent} to a capturing client if there is one.
*
- * @param displayType Should be a display type defined in {@code CarInputManager} such as
- * {@link CarInputManager#TARGET_DISPLAY_TYPE_MAIN}.
- * @param event
+ * @param displayType the display type defined in {@code CarInputManager} such as
+ * {@link CarInputManager#TARGET_DISPLAY_TYPE_MAIN}
+ * @param event the Rotary event to handle
+ *
* @return true if the event was consumed.
*/
public boolean onRotaryEvent(int displayType, RotaryEvent event) {
@@ -542,6 +550,37 @@
return true;
}
+ /**
+ * Dispatches the given {@link CustomInputEvent} to a capturing client if there is one.
+ * Nothing happens if no callback was registered for the incoming event. In this case this
+ * method will return {@code false}.
+ * <p>
+ * In case of there are more than one client registered for this event, then only the first one
+ * will be notified.
+ *
+ * @param event the {@link CustomInputEvent} to dispatch
+ * @return {@code true} if the event was consumed.
+ */
+ public boolean onCustomInputEvent(CustomInputEvent event) {
+ int displayType = event.getTargetDisplayType();
+ if (!SUPPORTED_DISPLAY_TYPES.contains(displayType)) {
+ Log.w(TAG, "onCustomInputEvent for not supported display:" + displayType);
+ return false;
+ }
+ ICarInputCallback callback;
+ synchronized (mLock) {
+ callback = getClientForInputTypeLocked(displayType,
+ CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT);
+ if (callback == null) {
+ Log.w(TAG, "No client for input: " + CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT
+ + " and display: " + displayType);
+ return false;
+ }
+ }
+ dispatchCustomInputEvent(displayType, event, callback);
+ return true;
+ }
+
ICarInputCallback getClientForInputTypeLocked(int targetDisplayType, int inputType) {
LinkedList<ClientInfoForDisplay> fullCapturersStack = mFullDisplayEventCapturers.get(
targetDisplayType);
@@ -571,14 +610,14 @@
public void dump(PrintWriter writer) {
writer.println("**InputCaptureClientController**");
synchronized (mLock) {
- for (int display: SUPPORTED_DISPLAY_TYPES) {
+ for (int display : SUPPORTED_DISPLAY_TYPES) {
writer.println("***Display:" + display);
HashMap<IBinder, ClientInfoForDisplay> allClientsForDisplay = mAllClients.get(
display);
if (allClientsForDisplay != null) {
writer.println("****All clients:");
- for (ClientInfoForDisplay client: allClientsForDisplay.values()) {
+ for (ClientInfoForDisplay client : allClientsForDisplay.values()) {
writer.println(client);
}
}
@@ -587,7 +626,7 @@
mFullDisplayEventCapturers.get(display);
if (fullCapturersStack != null) {
writer.println("****Full capture stack");
- for (ClientInfoForDisplay client: fullCapturersStack) {
+ for (ClientInfoForDisplay client : fullCapturersStack) {
writer.println(client);
}
}
@@ -599,7 +638,7 @@
LinkedList<ClientInfoForDisplay> perInputStack = perInputStacks.valueAt(i);
if (perInputStack.size() > 0) {
writer.println("**** Per Input stack, input type:" + inputType);
- for (ClientInfoForDisplay client: perInputStack) {
+ for (ClientInfoForDisplay client : perInputStack) {
writer.println(client);
}
}
@@ -659,7 +698,9 @@
try {
callback.onKeyEvents(targetDisplayType, mKeyEventDispatchScratchList);
} catch (RemoteException e) {
- // Ignore. Let death handler deal with it.
+ if (DBG_DISPATCH) {
+ Log.e(TAG, "Failed to dispatch KeyEvent " + event, e);
+ }
}
});
}
@@ -669,13 +710,36 @@
if (DBG_DISPATCH) {
Log.i(TAG, "dispatchRotaryEvent:" + event);
}
+ // TODO(b/159623196): Use HandlerThread for dispatching rather than relying on the main
+ // thread. Change here and other dispatch methods.
CarServiceUtils.runOnMain(() -> {
mRotaryEventDispatchScratchList.clear();
mRotaryEventDispatchScratchList.add(event);
try {
callback.onRotaryEvents(targetDisplayType, mRotaryEventDispatchScratchList);
} catch (RemoteException e) {
- // Ignore. Let death handler deal with it.
+ if (DBG_DISPATCH) {
+ Log.e(TAG, "Failed to dispatch RotaryEvent " + event, e);
+ }
+ }
+ });
+ }
+
+ private void dispatchCustomInputEvent(int targetDisplayType, CustomInputEvent event,
+ ICarInputCallback callback) {
+ if (DBG_DISPATCH) {
+ Log.d(TAG, "dispatchCustomInputEvent:" + event);
+ }
+ CarServiceUtils.runOnMain(() -> {
+ mCustomInputEventDispatchScratchList.clear();
+ mCustomInputEventDispatchScratchList.add(event);
+ try {
+ callback.onCustomInputEvents(targetDisplayType,
+ mCustomInputEventDispatchScratchList);
+ } catch (RemoteException e) {
+ if (DBG_DISPATCH) {
+ Log.e(TAG, "Failed to dispatch CustomInputEvent " + event, e);
+ }
}
});
}
diff --git a/service/src/com/android/car/hal/InputHalService.java b/service/src/com/android/car/hal/InputHalService.java
index 8cee8e9..a438e99 100644
--- a/service/src/com/android/car/hal/InputHalService.java
+++ b/service/src/com/android/car/hal/InputHalService.java
@@ -15,12 +15,16 @@
*/
package com.android.car.hal;
+import static android.hardware.automotive.vehicle.V2_0.CustomInputType.CUSTOM_EVENT_F1;
+import static android.hardware.automotive.vehicle.V2_0.CustomInputType.CUSTOM_EVENT_F10;
import static android.hardware.automotive.vehicle.V2_0.RotaryInputType.ROTARY_INPUT_TYPE_AUDIO_VOLUME;
import static android.hardware.automotive.vehicle.V2_0.RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION;
+import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_CUSTOM_INPUT;
import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_KEY_INPUT;
import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_ROTARY_INPUT;
import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
import android.car.input.RotaryEvent;
import android.hardware.automotive.vehicle.V2_0.VehicleDisplay;
import android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction;
@@ -54,7 +58,8 @@
private static final int[] SUPPORTED_PROPERTIES = new int[] {
HW_KEY_INPUT,
- HW_ROTARY_INPUT
+ HW_ROTARY_INPUT,
+ HW_CUSTOM_INPUT
};
private final VehicleHal mHal;
@@ -72,6 +77,8 @@
void onKeyEvent(KeyEvent event, int targetDisplay);
/** Called for rotary event */
void onRotaryEvent(RotaryEvent event, int targetDisplay);
+ /** Called for OEM custom input event */
+ void onCustomInputEvent(CustomInputEvent event);
}
/** The current press state of a key. */
@@ -87,10 +94,13 @@
private final Object mLock = new Object();
@GuardedBy("mLock")
- private boolean mKeyInputSupported = false;
+ private boolean mKeyInputSupported;
@GuardedBy("mLock")
- private boolean mRotaryInputSupported = false;
+ private boolean mRotaryInputSupported;
+
+ @GuardedBy("mLock")
+ private boolean mCustomInputSupported;
@GuardedBy("mLock")
private InputListener mListener;
@@ -114,8 +124,9 @@
public void setInputListener(InputListener listener) {
boolean keyInputSupported;
boolean rotaryInputSupported;
+ boolean customInputSupported;
synchronized (mLock) {
- if (!mKeyInputSupported && !mRotaryInputSupported) {
+ if (!mKeyInputSupported && !mRotaryInputSupported && !mCustomInputSupported) {
Log.w(CarLog.TAG_INPUT,
"input listener set while rotary and key input not supported");
return;
@@ -123,6 +134,7 @@
mListener = listener;
keyInputSupported = mKeyInputSupported;
rotaryInputSupported = mRotaryInputSupported;
+ customInputSupported = mCustomInputSupported;
}
if (keyInputSupported) {
mHal.subscribeProperty(this, HW_KEY_INPUT);
@@ -130,6 +142,9 @@
if (rotaryInputSupported) {
mHal.subscribeProperty(this, HW_ROTARY_INPUT);
}
+ if (customInputSupported) {
+ mHal.subscribeProperty(this, HW_CUSTOM_INPUT);
+ }
}
/** Returns whether {@code HW_KEY_INPUT} is supported. */
@@ -146,6 +161,13 @@
}
}
+ /** Returns whether {@code HW_CUSTOM_INPUT} is supported. */
+ public boolean isCustomInputSupported() {
+ synchronized (mLock) {
+ return mCustomInputSupported;
+ }
+ }
+
@Override
public void init() {
}
@@ -156,6 +178,7 @@
mListener = null;
mKeyInputSupported = false;
mRotaryInputSupported = false;
+ mCustomInputSupported = false;
}
}
@@ -178,6 +201,11 @@
mRotaryInputSupported = true;
}
break;
+ case HW_CUSTOM_INPUT:
+ synchronized (mLock) {
+ mCustomInputSupported = true;
+ }
+ break;
}
}
}
@@ -200,6 +228,9 @@
case HW_ROTARY_INPUT:
dispatchRotaryInput(listener, value);
break;
+ case HW_CUSTOM_INPUT:
+ dispatchCustomInput(listener, value);
+ break;
default:
Log.e(CarLog.TAG_INPUT,
"Wrong event dispatched, prop:0x" + Integer.toHexString(value.prop));
@@ -345,7 +376,6 @@
action,
code,
repeat,
- 0 /* meta state */,
0 /* deviceId */,
0 /* scancode */,
0 /* flags */,
@@ -357,6 +387,23 @@
listener.onKeyEvent(event, display);
}
+ private void dispatchCustomInput(InputListener listener, VehiclePropValue value) {
+ if (DBG) {
+ Log.d(CarLog.TAG_INPUT, "Dispatching CustomInputEvent for listener="
+ + listener + " and value=" + value);
+ }
+ int inputCode = value.value.int32Values.get(0);
+ int targetDisplayType = value.value.int32Values.get(1);
+ int repeatCounter = value.value.int32Values.get(2);
+
+ if (inputCode < CUSTOM_EVENT_F1 || inputCode > CUSTOM_EVENT_F10) {
+ Log.e(CarLog.TAG_INPUT, "Unknown custom input code: " + inputCode);
+ return;
+ }
+ CustomInputEvent event = new CustomInputEvent(inputCode, targetDisplayType, repeatCounter);
+ listener.onCustomInputEvent(event);
+ }
+
@Override
public void dump(PrintWriter writer) {
writer.println("*Input HAL*");
diff --git a/tests/CustomInputTestService/Android.bp b/tests/CustomInputTestService/Android.bp
new file mode 100644
index 0000000..18c45db
--- /dev/null
+++ b/tests/CustomInputTestService/Android.bp
@@ -0,0 +1,88 @@
+// Copyright (C) 2020 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.
+
+filegroup {
+ name: "CustomInputTestService-srcs",
+ srcs: [
+ "src/**/*.java",
+ ],
+}
+
+android_app {
+ name: "CustomInputTestService",
+ srcs: [":CustomInputTestService-srcs"],
+ resource_dirs: ["res"],
+
+ // Because it uses a platform API (CarInputManager).
+ platform_apis: true,
+
+ // This app should be platform signed because it requires android.permission.MONITOR_INPUT
+ // permission, which is of type "signature".
+ certificate: "platform",
+
+ optimize: {
+ enabled: false,
+ },
+
+ dex_preopt: {
+ enabled: false,
+ },
+
+ libs: [
+ "android.car",
+ ],
+
+ product_variables: {
+ pdk: {
+ enabled: false,
+ },
+ },
+}
+
+android_test {
+ name: "CustomInputTestServiceTest",
+
+ srcs: [
+ "tests/src/**/*.java",
+ ":CustomInputTestService-srcs",
+ ],
+
+ manifest: "tests/AndroidManifest.xml",
+
+ platform_apis: true,
+
+ static_libs: [
+ "mockito-target-extended",
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "truth-prebuilt",
+ ],
+
+ libs: [
+ "android.car",
+ "android.test.mock",
+ "android.test.base",
+ "android.test.runner",
+ ],
+
+ // Required by mockito-target-extended (lib used to mock final classes).
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ ],
+
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/tests/CustomInputTestService/AndroidManifest.xml b/tests/CustomInputTestService/AndroidManifest.xml
new file mode 100644
index 0000000..6aba2a7
--- /dev/null
+++ b/tests/CustomInputTestService/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.custominput.test">
+
+ <uses-permission android:name="android.permission.MONITOR_INPUT"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+
+ <application>
+ <service android:name=".CustomInputTestService"
+ android:exported="true" android:enabled="true" >
+ <intent-filter>
+ <action android:name="com.android.car.custominput.action.START_SILENT"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
+
diff --git a/tests/CustomInputTestService/readme.md b/tests/CustomInputTestService/readme.md
new file mode 100644
index 0000000..1b1d7a7
--- /dev/null
+++ b/tests/CustomInputTestService/readme.md
@@ -0,0 +1,52 @@
+# Custom Input Event
+
+## Building
+```bash
+make CustomInputTestService -j64
+```
+
+### Installing
+```bash
+adb install $OUT/target/product/emulator_car_x86/system/app/CustomInputTestService/CustomInputTestService.apk
+```
+
+## Start CustomInputTestService
+```bash
+adb shell am start-foreground-service com.android.car.custominput.test/.CustomInputTestService
+```
+
+### Running tests
+
+Steps to run unit tests:
+
+1. Build and install CustomInputTestService.apk (see above sections).
+1. Then run:
+
+```bash
+atest CustomInputTestServiceTest
+```
+
+## Inject events (test scripts)
+
+These are the test scripts to demonstrate how CustomInputEvent can be used to implement OEM
+partners non-standard events. They all represent hypothetical features for the sake of documentation
+ only.
+
+*Note*: Make sure CustomInputTestService is installed and started. Especially if you've just
+ ran tests. Depending on the configuration you use, running CustomInputTestServiceTest may
+ uninstall CustomInputTestService.
+
+### Inject Maps event from steering wheel control
+
+For this example, press home first, then inject the event to start Maps activity by running:
+
+```
+adb shell cmd car_service inject-custom-input -d 0 -customEvent f1
+```
+
+Parameters are:
+* `-d 0`: sets target display type to main display;
+* `-customEvent f1`: sets the OEM partner function to execute. In this implementation, `f1` argument
+ represents the action used to launch Google maps app;
+
+*Note*: For this command to run, make sure Google Maps app is installed first.
diff --git a/tests/CustomInputTestService/res/drawable/custom_input_ref_service.png b/tests/CustomInputTestService/res/drawable/custom_input_ref_service.png
new file mode 100644
index 0000000..4814ff1
--- /dev/null
+++ b/tests/CustomInputTestService/res/drawable/custom_input_ref_service.png
Binary files differ
diff --git a/tests/CustomInputTestService/res/values/strings.xml b/tests/CustomInputTestService/res/values/strings.xml
new file mode 100644
index 0000000..fd465e9
--- /dev/null
+++ b/tests/CustomInputTestService/res/values/strings.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<!-- TODO(b/159623196): set translatable="false" -->
+<!-- TODO(b/159623196): Move this to config.xml -->
+<!-- TODO(b/159623196): Specify this activity, follow the same example as in
+ https://source.corp.google.com/android/packages/services/Car/service/res/values/config.xml;l=55
+ -->
+<resources>
+ <string name="maps_app_package">"com.google.android.apps.maps"</string>
+ <string name="maps_activity_class">com.google.android.maps.MapsActivity</string>
+</resources>
diff --git a/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputEventListener.java b/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputEventListener.java
new file mode 100644
index 0000000..805565a
--- /dev/null
+++ b/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputEventListener.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 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.android.car.custominput.test;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.ActivityOptions;
+import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles incoming {@link CustomInputEvent}. In this implementation, incoming events are expected
+ * to have the display id set, the event input type is represented by the value passed in
+ * `-customEvent` flag (see {@link EventAction} for the available actions).
+ */
+final class CustomInputEventListener {
+
+ private static final String TAG = CustomInputEventListener.class.getSimpleName();
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final CustomInputTestService mService;
+ private final Context mContext;
+
+ /** List of defined actions for this reference service implementation */
+ @IntDef({EventAction.LAUNCH_MAPS_ACTION,
+ EventAction.ACCEPT_INCOMING_CALL_ACTION, EventAction.REJECT_INCOMING_CALL_ACTION,
+ EventAction.INCREASE_SOUND_VOLUME_ACTION, EventAction.DECREASE_SOUND_VOLUME_ACTION})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventAction {
+
+ /** Launches Map action. */
+ int LAUNCH_MAPS_ACTION = CustomInputEvent.INPUT_CODE_F1;
+
+ /** Accepts incoming call action. */
+ int ACCEPT_INCOMING_CALL_ACTION = CustomInputEvent.INPUT_CODE_F2;
+
+ /** Rejects incoming call action. */
+ int REJECT_INCOMING_CALL_ACTION = CustomInputEvent.INPUT_CODE_F3;
+
+ /** Increases volume action. */
+ int INCREASE_SOUND_VOLUME_ACTION = CustomInputEvent.INPUT_CODE_F4;
+
+ /** Increases volume action. */
+ int DECREASE_SOUND_VOLUME_ACTION = CustomInputEvent.INPUT_CODE_F5;
+ }
+
+ CustomInputEventListener(
+ @NonNull Context context,
+ @NonNull CustomInputTestService service) {
+ mContext = context;
+ mService = service;
+ }
+
+ void handle(int targetDisplayType, CustomInputEvent event) {
+ if (!isValidTargetDisplayType(targetDisplayType)) {
+ return;
+ }
+ int targetDisplayId = getDisplayIdForDisplayType(targetDisplayType);
+ @EventAction int action = event.getInputCode();
+ switch (action) {
+ case EventAction.LAUNCH_MAPS_ACTION:
+ launchMap(targetDisplayId);
+ break;
+ case EventAction.ACCEPT_INCOMING_CALL_ACTION:
+ acceptIncomingCall(targetDisplayId);
+ break;
+ case EventAction.REJECT_INCOMING_CALL_ACTION:
+ rejectIncomingCall(targetDisplayId);
+ break;
+ case EventAction.INCREASE_SOUND_VOLUME_ACTION:
+ increaseVolume(targetDisplayId);
+ break;
+ case EventAction.DECREASE_SOUND_VOLUME_ACTION:
+ decreaseVolume(targetDisplayId);
+ break;
+ default:
+ Log.e(TAG, "Ignoring event [" + action + "]");
+ }
+ }
+
+ private int getDisplayIdForDisplayType(/* unused for now */ int targetDisplayType) {
+ // TODO(159623196): convert the displayType to displayId using OccupantZoneManager api and
+ // add tests. For now, we're just returning the display type.
+ return 0; // Hardcoded to return main display id for now.
+ }
+
+ private static boolean isValidTargetDisplayType(int displayType) {
+ if (displayType == CarInputManager.TARGET_DISPLAY_TYPE_MAIN) {
+ return true;
+ }
+ Log.w(TAG,
+ "This service implementation can only handle CustomInputEvent with "
+ + "targetDisplayType set to main display (main display type is {"
+ + CarInputManager.TARGET_DISPLAY_TYPE_MAIN + "}), current display type is {"
+ + displayType + "})");
+ return false;
+ }
+
+ private void launchMap(int targetDisplayId) {
+ if (DEBUG) {
+ Log.d(TAG, "Launching Maps on display {" + targetDisplayId + "}");
+ }
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(targetDisplayId);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW);
+ mapsIntent.setClassName(mContext.getString(R.string.maps_app_package),
+ mContext.getString(R.string.maps_activity_class));
+ mapsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ mService.startActivityAsUser(mapsIntent, options.toBundle(), UserHandle.CURRENT);
+ }
+
+ private void acceptIncomingCall(int targetDisplayId) {
+ // TODO(b/159623196): When implementing this method, avoid using
+ // TelecomManager#acceptRingingCall deprecated method.
+ if (DEBUG) {
+ Log.d(TAG, "Accepting incoming call on display {" + targetDisplayId + "}");
+ }
+ }
+
+ private void rejectIncomingCall(int targetDisplayId) {
+ // TODO(b/159623196): When implementing this method, avoid using
+ // TelecomManager#endCall deprecated method.
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting incoming call on display {" + targetDisplayId + "}");
+ }
+ }
+
+ private void increaseVolume(int targetDisplayId) {
+ // TODO(b/159623196): Provide implementation.
+ if (DEBUG) {
+ Log.d(TAG, "Increasing volume on display {" + targetDisplayId + "}");
+ }
+ }
+
+ private void decreaseVolume(int targetDisplayId) {
+ // TODO(kanant, b/159623196): Provide implementation.
+ if (DEBUG) {
+ Log.d(TAG, "Decreasing volume on display {" + targetDisplayId + "}");
+ }
+ }
+}
diff --git a/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputTestService.java b/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputTestService.java
new file mode 100644
index 0000000..0c41eb2
--- /dev/null
+++ b/tests/CustomInputTestService/src/com/android/car/custominput/test/CustomInputTestService.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 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.android.car.custominput.test;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.car.Car;
+import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * This service is a reference implementation to be used as an example on how to define and handle
+ * HW_CUSTOM_INPUT events.
+ */
+// TODO(b/12219669): Rename this to CustomInputSampleService
+public class CustomInputTestService extends Service implements
+ CarInputManager.CarInputCaptureCallback {
+
+ private static final String TAG = CustomInputTestService.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final String CHANNEL_ID = CustomInputTestService.class.getSimpleName();
+ private static final int FOREGROUND_ID = 1;
+
+ private Car mCar;
+ private CarInputManager mCarInputManager;
+ private CustomInputEventListener mEventHandler;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ startForeground();
+ }
+
+ private void startForeground() {
+ // TODO(b/12219669): Start this service from carservice, then the code in below
+ // won't be needed.
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
+ CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ Notification notification = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
+ .setContentTitle("CustomInputTestService")
+ .setContentText("Processing...")
+ .setSmallIcon(R.drawable.custom_input_ref_service)
+ .build();
+ startForeground(FOREGROUND_ID, notification);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ connectToCarService();
+ return START_STICKY;
+ }
+
+ private void connectToCarService() {
+ if (mCar != null && mCar.isConnected()) {
+ Log.w(TAG, "Ignoring request to connect against car service");
+ return;
+ }
+ Log.i(TAG, "Connecting against car service");
+ mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+ (car, ready) -> {
+ mCar = car;
+ if (ready) {
+ mCarInputManager =
+ (CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);
+ mCarInputManager.requestInputEventCapture(this,
+ CarInputManager.TARGET_DISPLAY_TYPE_MAIN,
+ new int[]{CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT},
+ CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT);
+ }
+ });
+ mEventHandler = new CustomInputEventListener(getApplicationContext(), this);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) {
+ Log.d(TAG, "Service destroyed");
+ }
+ if (mCarInputManager != null) {
+ mCarInputManager.releaseInputEventCapture(CarInputManager.TARGET_DISPLAY_TYPE_MAIN);
+ }
+ if (mCar != null) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCustomInputEvents(int targetDisplayId, @NonNull List<CustomInputEvent> events) {
+ for (CustomInputEvent event : events) {
+ mEventHandler.handle(targetDisplayId, event);
+ }
+ }
+}
diff --git a/tests/CustomInputTestService/tests/AndroidManifest.xml b/tests/CustomInputTestService/tests/AndroidManifest.xml
new file mode 100644
index 0000000..d32e506
--- /dev/null
+++ b/tests/CustomInputTestService/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.custominput.test" >
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.car.custominput.test"
+ android:label="Unit Tests for CustomInputTestService"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner"/>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/tests/CustomInputTestService/tests/src/com/android/car/custominput/test/CustomInputEventListenerTest.java b/tests/CustomInputTestService/tests/src/com/android/car/custominput/test/CustomInputEventListenerTest.java
new file mode 100644
index 0000000..b4e1e4f
--- /dev/null
+++ b/tests/CustomInputTestService/tests/src/com/android/car/custominput/test/CustomInputEventListenerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 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.android.car.custominput.test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CustomInputEventListenerTest {
+
+ private CustomInputEventListener mEventHandler;
+
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private CustomInputTestService mService;
+
+ @Before
+ public void setUp() {
+ when(mContext.getString(R.string.maps_app_package)).thenReturn(
+ "com.google.android.apps.maps");
+ when(mContext.getString(R.string.maps_activity_class)).thenReturn(
+ "com.google.android.maps.MapsActivity");
+
+ mEventHandler = new CustomInputEventListener(mContext, mService);
+ }
+
+ @Test
+ public void testHandleEvent_launchingMaps() {
+ // Arrange
+ int anyDisplayId = CarInputManager.TARGET_DISPLAY_TYPE_MAIN;
+ CustomInputEvent event = new CustomInputEvent(
+ // In this implementation, INPUT_TYPE_CUSTOM_EVENT_F1 represents the action of
+ // launching maps.
+ /* inputCode= */ CustomInputEvent.INPUT_CODE_F1,
+ /* targetDisplayType= */ anyDisplayId,
+ /* repeatCounter= */ 1);
+
+ // Act
+ mEventHandler.handle(anyDisplayId, event);
+
+ // Assert
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+ ArgumentCaptor<UserHandle> userHandleCaptor = ArgumentCaptor.forClass(UserHandle.class);
+ verify(mService).startActivityAsUser(intentCaptor.capture(),
+ bundleCaptor.capture(), userHandleCaptor.capture());
+
+ // Assert intent parameter
+ Intent actualIntent = intentCaptor.getValue();
+ assertThat(actualIntent.getAction()).isEqualTo(Intent.ACTION_VIEW);
+ assertThat(actualIntent.getFlags()).isEqualTo(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ assertThat(actualIntent.getComponent()).isEqualTo(
+ new ComponentName("com.google.android.apps.maps",
+ "com.google.android.maps.MapsActivity"));
+
+ // Assert bundle and user parameters
+ assertThat(bundleCaptor.getValue().getInt("android.activity.launchDisplayId")).isEqualTo(
+ /* displayId= */
+ 0); // TODO(b/159623196): displayId is currently hardcoded to 0, see missing
+ // targetDisplayTarget to targetDisplayId logic in
+ // CustomInputEventListener
+ assertThat(userHandleCaptor.getValue()).isEqualTo(UserHandle.CURRENT);
+ }
+
+ @Test
+ public void testHandleEvent_ignoringEventsForNonMainDisplay() {
+ int invalidDisplayId = -1;
+ CustomInputEvent event = new CustomInputEvent(CustomInputEvent.INPUT_CODE_F1,
+ invalidDisplayId,
+ /* repeatCounter= */ 1);
+
+ // Act
+ mEventHandler.handle(invalidDisplayId, event);
+
+ // Assert
+ verify(mService, never()).startActivityAsUser(any(Intent.class), any(Bundle.class),
+ any(UserHandle.class));
+ }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/VehiclePropertyIdsTest.java b/tests/android_car_api_test/src/android/car/apitest/VehiclePropertyIdsTest.java
index 98451fb..14a38fe 100644
--- a/tests/android_car_api_test/src/android/car/apitest/VehiclePropertyIdsTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/VehiclePropertyIdsTest.java
@@ -38,12 +38,14 @@
new ArrayList<>(
Arrays.asList(
"DISABLED_OPTIONAL_FEATURES",
+ "HW_CUSTOM_INPUT",
"HW_ROTARY_INPUT",
"SUPPORT_CUSTOMIZE_VENDOR_PERMISSION"));
private static final List<Integer> MISSING_VEHICLE_PROPERTY_ID_VALUES =
new ArrayList<>(
Arrays.asList(
/*DISABLED_OPTIONAL_FEATURES=*/286265094,
+ /*HW_CUSTOM_INPUT=*/289475120,
/*HW_ROTARY_INPUT=*/289475104,
/*SUPPORT_CUSTOMIZE_VENDOR_PERMISSION=*/287313669));
diff --git a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
index 907bdc2..b654fe7 100644
--- a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
@@ -23,6 +23,7 @@
import android.annotation.NonNull;
import android.car.Car;
import android.car.input.CarInputManager;
+import android.car.input.CustomInputEvent;
import android.car.input.RotaryEvent;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.util.Log;
@@ -47,6 +48,7 @@
import java.util.concurrent.TimeUnit;
+// TODO(b/159623196): Enhance this class to test HW_CUSTOM_INPUT
@RunWith(AndroidJUnit4.class)
@MediumTest
public final class CarInputManagerTest extends MockedCarTestBase {
@@ -72,6 +74,11 @@
private final LinkedList<Pair<Integer, List<RotaryEvent>>> mRotaryEvents =
new LinkedList<>();
+ // Stores passed events. Last one in front
+ @GuardedBy("mLock")
+ private final LinkedList<Pair<Integer, List<CustomInputEvent>>> mCustomInputEvents =
+ new LinkedList<>();
+
// Stores passed state changes. Last one in front
@GuardedBy("mLock")
private final LinkedList<Pair<Integer, int[]>> mStateChanges = new LinkedList<>();
@@ -79,6 +86,7 @@
private final Semaphore mKeyEventWait = new Semaphore(0);
private final Semaphore mRotaryEventWait = new Semaphore(0);
private final Semaphore mStateChangeWait = new Semaphore(0);
+ private final Semaphore mCustomInputEventWait = new Semaphore(0);
@Override
public void onKeyEvents(int targetDisplayId, List<KeyEvent> keyEvents) {
@@ -100,6 +108,16 @@
}
@Override
+ public void onCustomInputEvents(int targetDisplayId, List<CustomInputEvent> events) {
+ Log.i(TAG, "onCustomInputEvents event:" + events.get(0) + " this:" + this);
+ synchronized (mLock) {
+ mCustomInputEvents.addFirst(new Pair<Integer, List<CustomInputEvent>>(
+ targetDisplayId, events));
+ }
+ mCustomInputEventWait.release();
+ }
+
+ @Override
public void onCaptureStateChanged(int targetDisplayId,
@NonNull @CarInputManager.InputTypeEnum int[] activeInputTypes) {
Log.i(TAG, "onCaptureStateChanged types:" + Arrays.toString(activeInputTypes)
@@ -128,6 +146,10 @@
mRotaryEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
}
+ private void waitForCustomInputEvent() throws Exception {
+ mCustomInputEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+ }
+
private LinkedList<Pair<Integer, List<KeyEvent>>> getkeyEvents() {
synchronized (mLock) {
LinkedList<Pair<Integer, List<KeyEvent>>> r =
@@ -155,6 +177,7 @@
private final CaptureCallback mCallback0 = new CaptureCallback();
private final CaptureCallback mCallback1 = new CaptureCallback();
+ private final CaptureCallback mCallback2 = new CaptureCallback();
@Override
protected synchronized void configureMockedHal() {
@@ -166,6 +189,10 @@
VehiclePropValueBuilder.newBuilder(VehicleProperty.HW_ROTARY_INPUT)
.addIntValue(0, 1, 0)
.build());
+ addProperty(VehicleProperty.HW_CUSTOM_INPUT,
+ VehiclePropValueBuilder.newBuilder(VehicleProperty.HW_CUSTOM_INPUT)
+ .addIntValue(0)
+ .build());
}
@Override
@@ -337,6 +364,11 @@
CarInputManager.TARGET_DISPLAY_TYPE_MAIN,
new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION}, 0);
assertThat(r).isEqualTo(CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED);
+
+ r = mCarInputManager.requestInputEventCapture(mCallback2,
+ CarInputManager.TARGET_DISPLAY_TYPE_MAIN,
+ new int[]{CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT}, 0);
+ assertThat(r).isEqualTo(CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED);
}
@Test
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
index 09c3d4a..0bbb23d 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
@@ -15,6 +15,8 @@
*/
package com.android.car.hal;
+import static android.hardware.automotive.vehicle.V2_0.CustomInputType.CUSTOM_EVENT_F1;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -27,6 +29,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.car.input.CustomInputEvent;
import android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction;
import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
@@ -63,6 +66,8 @@
VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_KEY_INPUT).build();
private static final VehiclePropConfig HW_ROTARY_INPUT_CONFIG =
VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_ROTARY_INPUT).build();
+ private static final VehiclePropConfig HW_CUSTOM_INPUT_CONFIG =
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_CUSTOM_INPUT).build();
private static final int DISPLAY = 42;
private enum Key { DOWN, UP }
@@ -104,6 +109,7 @@
assertThat(mInputHalService.isKeyInputSupported()).isTrue();
assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
+ assertThat(mInputHalService.isCustomInputSupported()).isFalse();
}
@Test
@@ -117,20 +123,37 @@
assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
assertThat(mInputHalService.isKeyInputSupported()).isFalse();
+ assertThat(mInputHalService.isCustomInputSupported()).isFalse();
}
@Test
- public void takesKeyAndRotaryInputProperty() {
+ public void takesCustomInputProperty() {
+ Set<VehiclePropConfig> offeredProps = ImmutableSet.of(
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build(),
+ HW_CUSTOM_INPUT_CONFIG,
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.CURRENT_GEAR).build());
+
+ mInputHalService.takeProperties(offeredProps);
+
+ assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
+ assertThat(mInputHalService.isKeyInputSupported()).isFalse();
+ assertThat(mInputHalService.isCustomInputSupported()).isTrue();
+ }
+
+ @Test
+ public void takesKeyAndRotaryAndCustomInputProperty() {
Set<VehiclePropConfig> offeredProps = ImmutableSet.of(
VehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build(),
HW_KEY_INPUT_CONFIG,
HW_ROTARY_INPUT_CONFIG,
+ HW_CUSTOM_INPUT_CONFIG,
VehiclePropConfigBuilder.newBuilder(VehicleProperty.CURRENT_GEAR).build());
mInputHalService.takeProperties(offeredProps);
assertThat(mInputHalService.isKeyInputSupported()).isTrue();
assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
+ assertThat(mInputHalService.isCustomInputSupported()).isTrue();
}
@Test
@@ -285,7 +308,8 @@
assertThat(upEvent.getAction()).isEqualTo(KeyEvent.ACTION_UP);
assertThat(upEvent.getEventTime()).isEqualTo(timestampMillis);
- events.forEach(KeyEvent::recycle);*/
+ events.forEach(KeyEvent::recycle);
+ */
}
@Test
@@ -325,6 +349,32 @@
events.forEach(KeyEvent::recycle);*/
}
+ @Test
+ public void dispatchesCustomInputEvent() {
+ // Arrange mInputListener to capture incoming CustomInputEvent
+ subscribeListener();
+
+ List<CustomInputEvent> events = new ArrayList<>();
+ doAnswer(invocation -> {
+ CustomInputEvent event = invocation.getArgument(0);
+ events.add(event);
+ return null;
+ }).when(mInputListener).onCustomInputEvent(any());
+
+ // Arrange
+ int targetDisplayType = InputHalService.DISPLAY_INSTRUMENT_CLUSTER;
+ int repeatCounter = 1;
+ VehiclePropValue customInputPropValue = makeCustomInputPropValue(
+ CUSTOM_EVENT_F1, targetDisplayType, repeatCounter);
+
+ // Act
+ mInputHalService.onHalEvents(ImmutableList.of(customInputPropValue));
+
+ // Assert
+ assertThat(events).containsExactly(new CustomInputEvent(
+ CustomInputEvent.INPUT_CODE_F1, targetDisplayType, repeatCounter));
+ }
+
private void subscribeListener() {
mInputHalService.takeProperties(ImmutableSet.of(HW_KEY_INPUT_CONFIG));
assertThat(mInputHalService.isKeyInputSupported()).isTrue();
@@ -389,4 +439,14 @@
v.timestamp = timestamp;
return v;
}
+
+ private VehiclePropValue makeCustomInputPropValue(int inputCode, int targetDisplayType,
+ int repeatCounter) {
+ VehiclePropValue v = new VehiclePropValue();
+ v.prop = VehicleProperty.HW_CUSTOM_INPUT;
+ v.value.int32Values.add(inputCode);
+ v.value.int32Values.add(targetDisplayType);
+ v.value.int32Values.add(repeatCounter);
+ return v;
+ }
}
\ No newline at end of file