resolve merge conflicts of 235f8ac to nyc-car-dev
Change-Id: I49327e591ef08875a92928535a1a0c69f7368804
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 5d56948..e0d91f4 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -155,8 +155,9 @@
@SystemApi
public static final String PERMISSION_CAR_RADIO = "android.car.permission.CAR_RADIO";
+
/**
- * Permission necesary to access Car PRJECTION system APIs.
+ * Permission necessary to access Car PROJECTION system APIs.
* @hide
*/
@SystemApi
diff --git a/car-lib/src/android/car/CarAppContextManager.java b/car-lib/src/android/car/CarAppContextManager.java
index c7657ca..22e36bf 100644
--- a/car-lib/src/android/car/CarAppContextManager.java
+++ b/car-lib/src/android/car/CarAppContextManager.java
@@ -51,7 +51,7 @@
/**
* Lost ownership for the context, which happens when other app has set the context.
* The app losing context should stop the action associated with the context.
- * For example, navigaiton app currently running active navigation should stop navigation
+ * For example, navigation app currently running active navigation should stop navigation
* upon getting this for {@link CarAppContextManager#APP_CONTEXT_NAVIGATION}.
* @param context
*/
@@ -89,7 +89,7 @@
mService = IAppContext.Stub.asInterface(service);
mHandler = new Handler(looper);
mBinderListener = new IAppContextListenerImpl(this);
- mOwnershipListeners = new HashMap<Integer, AppContextOwnershipChangeListener>();
+ mOwnershipListeners = new HashMap<>();
}
/**
diff --git a/car-support-lib/api/current.txt b/car-support-lib/api/current.txt
index 583fdc9..8024aaa 100644
--- a/car-support-lib/api/current.txt
+++ b/car-support-lib/api/current.txt
@@ -18,6 +18,7 @@
field public static final int CONNECTION_TYPE_EMBEDDED = 5; // 0x5
field public static final int CONNECTION_TYPE_EMULATOR = 0; // 0x0
field public static final int CONNECTION_TYPE_ON_DEVICE_EMULATOR = 3; // 0x3
+ field public static final int CONNECTION_TYPE_UNKNOWN = -1; // 0xffffffff
field public static final int CONNECTION_TYPE_USB = 1; // 0x1
field public static final int CONNECTION_TYPE_WIFI = 2; // 0x2
field public static final java.lang.String INFO_SERVICE = "info";
@@ -60,6 +61,14 @@
method public abstract java.lang.Integer getInt(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
method public abstract java.lang.Long getLong(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
method public abstract java.lang.String getString(java.lang.String) throws android.support.car.CarNotConnectedException, java.lang.IllegalArgumentException;
+ field public static final int DRIVER_SIDE_CENTER = 2; // 0x2
+ field public static final int DRIVER_SIDE_LEFT = 0; // 0x0
+ field public static final int DRIVER_SIDE_RIGHT = 1; // 0x1
+ field public static final java.lang.String KEY_DRIVER_POSITION = "driverPosition";
+ field public static final java.lang.String KEY_HEAD_UNIT_MAKE = "headUnitMake";
+ field public static final java.lang.String KEY_HEAD_UNIT_MODEL = "headUnitModel";
+ field public static final java.lang.String KEY_HEAD_UNIT_SOFTWARE_BUILD = "headUnitSoftwareBuild";
+ field public static final java.lang.String KEY_HEAD_UNIT_SOFTWARE_VERSION = "headUnitSoftwareVersion";
field public static final java.lang.String KEY_MANUFACTURER = "manufacturer";
field public static final java.lang.String KEY_MODEL = "model";
field public static final java.lang.String KEY_MODEL_YEAR = "model-year";
diff --git a/car-support-lib/src/android/support/car/Car.java b/car-support-lib/src/android/support/car/Car.java
index e5739fd..6218288 100644
--- a/car-support-lib/src/android/support/car/Car.java
+++ b/car-support-lib/src/android/support/car/Car.java
@@ -80,6 +80,8 @@
public static final int CONNECTION_TYPE_ADB_EMULATOR = 4;
/** Type of car connection: platform runs directly in car. */
public static final int CONNECTION_TYPE_EMBEDDED = 5;
+ /** Unknown type. The support lib is likely out of date.*/
+ public static final int CONNECTION_TYPE_UNKNOWN = -1;
/**
* Type of car connection: platform runs directly in car but with mocked vehicle hal.
* This will only happen in testing environment.
@@ -89,7 +91,8 @@
/** @hide */
@IntDef({CONNECTION_TYPE_EMULATOR, CONNECTION_TYPE_USB, CONNECTION_TYPE_WIFI,
- CONNECTION_TYPE_ON_DEVICE_EMULATOR, CONNECTION_TYPE_ADB_EMULATOR, CONNECTION_TYPE_EMBEDDED})
+ CONNECTION_TYPE_ON_DEVICE_EMULATOR, CONNECTION_TYPE_ADB_EMULATOR,
+ CONNECTION_TYPE_EMBEDDED, CONNECTION_TYPE_UNKNOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface ConnectionType {}
diff --git a/car-support-lib/src/android/support/car/CarAppContextManager.java b/car-support-lib/src/android/support/car/CarAppContextManager.java
index 70616b9..64198eb 100644
--- a/car-support-lib/src/android/support/car/CarAppContextManager.java
+++ b/car-support-lib/src/android/support/car/CarAppContextManager.java
@@ -42,7 +42,7 @@
/**
* Lost ownership for the context, which happens when other app has set the context.
* The app losing context should stop the action associated with the context.
- * For example, navigaiton app currently running active navigation should stop navigation
+ * For example, navigation app currently running active navigation should stop navigation
* upon getting this for {@link CarAppContextManager#APP_CONTEXT_NAVIGATION}.
* @param context
*/
diff --git a/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java b/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
index a610fe4..cd145ac 100644
--- a/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
+++ b/car-support-lib/src/android/support/car/CarAppContextManagerEmbedded.java
@@ -34,7 +34,7 @@
*/
CarAppContextManagerEmbedded(Object manager) {
mManager = (android.car.CarAppContextManager) manager;
- mOwnershipListeners = new HashMap<Integer, AppContextOwnershipChangeListenerProxy>();
+ mOwnershipListeners = new HashMap<>();
}
@Override
diff --git a/car-support-lib/src/android/support/car/CarInfoManager.java b/car-support-lib/src/android/support/car/CarInfoManager.java
index 78eacac..b40f7d3 100644
--- a/car-support-lib/src/android/support/car/CarInfoManager.java
+++ b/car-support-lib/src/android/support/car/CarInfoManager.java
@@ -54,22 +54,50 @@
@ValueTypeDef(type = String.class)
public static final String KEY_VEHICLE_ID = "vehicle-id";
+ /** Manufacturer of the head unit.*/
+ @ValueTypeDef(type = String.class)
+ public static final String KEY_HEAD_UNIT_MAKE = "headUnitMake";
+ /** Model of the head unit.*/
+ @ValueTypeDef(type = String.class)
+ public static final String KEY_HEAD_UNIT_MODEL = "headUnitModel";
+ /** Head Unit software build */
+ @ValueTypeDef(type = String.class)
+ public static final String KEY_HEAD_UNIT_SOFTWARE_BUILD = "headUnitSoftwareBuild";
+ /** Head Unit software version */
+ @ValueTypeDef(type = String.class)
+ public static final String KEY_HEAD_UNIT_SOFTWARE_VERSION = "headUnitSoftwareVersion";
+ /** Where is the driver's seat. One of the DRIVER_SIDE_* constants */
+ @ValueTypeDef(type = Integer.class)
+ public static final String KEY_DRIVER_POSITION = "driverPosition";
+
+ /** Location of the driver: left */
+ public static final int DRIVER_SIDE_LEFT = 0;
+ /** Location of the driver: right */
+ public static final int DRIVER_SIDE_RIGHT = 1;
+ /** Location of the driver: center */
+ public static final int DRIVER_SIDE_CENTER = 2;
+
/**
- * Retrieve floating point information for car.
- * @param key
- * @return null if the key is not supported.
- * @throws CarNotConnectedException
- * @throws IllegalArgumentException
+ * Returns the value for the given key or null if the key is not supported.
*/
public abstract Float getFloat(String key)
throws CarNotConnectedException, IllegalArgumentException;
+ /**
+ * Returns the value for the given key or null if the key is not supported.
+ */
public abstract Integer getInt(String key)
throws CarNotConnectedException, IllegalArgumentException;
+ /**
+ * Returns the value for the given key or null if the key is not supported.
+ */
public abstract Long getLong(String key)
throws CarNotConnectedException, IllegalArgumentException;
+ /**
+ * Returns the value for the given key or null if the key is not supported.
+ */
public abstract String getString(String key)
throws CarNotConnectedException, IllegalArgumentException;
@@ -78,9 +106,6 @@
* defined only for the car vendor. Vendor extension can be used for other APIs like
* getInt / getString, but this is for passing more complex data.
* @param key
- * @return
- * @throws CarNotConnectedException
- * @throws IllegalArgumentException
* @hide
*/
public abstract Bundle getBundle(String key)
diff --git a/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java b/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
index fe0338e..f22b95a 100644
--- a/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
+++ b/car-support-lib/src/android/support/car/CarServiceLoaderEmbedded.java
@@ -79,7 +79,9 @@
@Override
public int getCarConnectionType() throws CarNotConnectedException {
- return mCar.getCarConnectionType();
+ @android.support.car.Car.ConnectionType
+ int carConnectionType = mCar.getCarConnectionType();
+ return carConnectionType;
}
@Override
@@ -113,20 +115,20 @@
}
// For publicly available versions, return wrapper version.
switch (serviceName) {
- case Car.AUDIO_SERVICE:
- return new CarAudioManagerEmbedded(manager);
- case Car.SENSOR_SERVICE:
- return new CarSensorManagerEmbedded(manager);
- case Car.INFO_SERVICE:
- return new CarInfoManagerEmbedded(manager);
- case Car.APP_CONTEXT_SERVICE:
- return new CarAppContextManagerEmbedded(manager);
- case Car.PACKAGE_SERVICE:
- return new CarPackageManagerEmbedded(manager);
- case Car.CAR_NAVIGATION_SERVICE:
- return new CarNavigationManagerEmbedded(manager);
- default:
- return manager;
+ case Car.AUDIO_SERVICE:
+ return new CarAudioManagerEmbedded(manager);
+ case Car.SENSOR_SERVICE:
+ return new CarSensorManagerEmbedded(manager, getContext());
+ case Car.INFO_SERVICE:
+ return new CarInfoManagerEmbedded(manager);
+ case Car.APP_CONTEXT_SERVICE:
+ return new CarAppContextManagerEmbedded(manager);
+ case Car.PACKAGE_SERVICE:
+ return new CarPackageManagerEmbedded(manager);
+ case Car.CAR_NAVIGATION_SERVICE:
+ return new CarNavigationManagerEmbedded(manager);
+ default:
+ return manager;
}
}
diff --git a/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java b/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
index e4a7a06..dbffb13 100644
--- a/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
+++ b/car-support-lib/src/android/support/car/hardware/CarSensorManagerEmbedded.java
@@ -16,35 +16,71 @@
package android.support.car.hardware;
+import android.content.Context;
import android.support.car.CarNotConnectedException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Arrays;
import java.util.LinkedList;
+import java.util.Set;
/**
* @hide
*/
public class CarSensorManagerEmbedded extends CarSensorManager {
+ private static final String TAG = "CarSensorsProxy";
private final android.car.hardware.CarSensorManager mManager;
+ private final CarSensorsProxy mCarSensorsProxy;
private final LinkedList<CarSensorEventListenerProxy> mListeners = new LinkedList<>();
- public CarSensorManagerEmbedded(Object manager) {
+ public CarSensorManagerEmbedded(Object manager, Context context) {
mManager = (android.car.hardware.CarSensorManager) manager;
+ mCarSensorsProxy = new CarSensorsProxy(context);
}
@Override
public int[] getSupportedSensors() throws CarNotConnectedException {
try {
- return mManager.getSupportedSensors();
+ Set<Integer> sensorsSet = new HashSet<Integer>();
+ for (Integer sensor : mManager.getSupportedSensors()) {
+ sensorsSet.add(sensor);
+ }
+ for (Integer proxySensor : mCarSensorsProxy.getSupportedSensors()) {
+ sensorsSet.add(proxySensor);
+ }
+ return toIntArray(sensorsSet);
} catch (android.car.CarNotConnectedException e) {
throw new CarNotConnectedException(e);
}
}
+ private static int[] toIntArray(Collection<Integer> collection) {
+ int len = collection.size();
+ int[] arr = new int[len];
+ int arrIndex = 0;
+ for (Integer item : collection) {
+ arr[arrIndex] = item;
+ arrIndex++;
+ }
+ return arr;
+ }
+
@Override
public boolean isSensorSupported(int sensorType) throws CarNotConnectedException {
try {
- return mManager.isSensorSupported(sensorType);
+ return mManager.isSensorSupported(sensorType)
+ || mCarSensorsProxy.isSensorSupported(sensorType);
+ } catch (android.car.CarNotConnectedException e) {
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ private boolean isSensorProxied(int sensorType) throws CarNotConnectedException {
+ try {
+ return !mManager.isSensorSupported(sensorType)
+ && mCarSensorsProxy.isSensorSupported(sensorType);
} catch (android.car.CarNotConnectedException e) {
throw new CarNotConnectedException(e);
}
@@ -53,13 +89,17 @@
@Override
public boolean registerListener(CarSensorEventListener listener, int sensorType,
int rate) throws CarNotConnectedException, IllegalArgumentException {
+ if (isSensorProxied(sensorType)) {
+ return mCarSensorsProxy.registerSensorListener(listener, sensorType, rate);
+ }
CarSensorEventListenerProxy proxy = null;
synchronized (this) {
proxy = findListenerLocked(listener);
if (proxy == null) {
proxy = new CarSensorEventListenerProxy(listener, sensorType);
+ mListeners.add(proxy);
} else {
- proxy.sensors |= sensorType;
+ proxy.sensors.add(sensorType);
}
}
try {
@@ -72,6 +112,7 @@
@Override
public void unregisterListener(CarSensorEventListener listener)
throws CarNotConnectedException {
+ mCarSensorsProxy.unregisterSensorListener(listener);
CarSensorEventListenerProxy proxy = null;
synchronized (this) {
proxy = findListenerLocked(listener);
@@ -90,14 +131,15 @@
@Override
public void unregisterListener(CarSensorEventListener listener, int sensorType)
throws CarNotConnectedException {
+ mCarSensorsProxy.unregisterSensorListener(listener, sensorType);
CarSensorEventListenerProxy proxy = null;
synchronized (this) {
proxy = findListenerLocked(listener);
if (proxy == null) {
return;
}
- proxy.sensors &= ~sensorType;
- if (proxy.sensors == 0) {
+ proxy.sensors.remove(sensorType);
+ if (proxy.sensors.isEmpty()) {
mListeners.remove(proxy);
}
}
@@ -110,6 +152,9 @@
@Override
public CarSensorEvent getLatestSensorEvent(int type) throws CarNotConnectedException {
+ if (isSensorProxied(type)) {
+ return mCarSensorsProxy.getLatestSensorEvent(type);
+ }
try {
return convert(mManager.getLatestSensorEvent(type));
} catch (android.car.CarNotConnectedException e) {
@@ -143,11 +188,11 @@
android.car.hardware.CarSensorManager.CarSensorEventListener {
public final CarSensorEventListener listener;
- public int sensors;
+ public final Set<Integer> sensors = new HashSet<Integer>();
- public CarSensorEventListenerProxy(CarSensorEventListener listener, int sensors) {
+ CarSensorEventListenerProxy(CarSensorEventListener listener, int sensor) {
this.listener = listener;
- this.sensors = sensors;
+ this.sensors.add(sensor);
}
@Override
diff --git a/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java b/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java
new file mode 100644
index 0000000..6d5acc3
--- /dev/null
+++ b/car-support-lib/src/android/support/car/hardware/CarSensorsProxy.java
@@ -0,0 +1,489 @@
+/*
+ * 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 android.support.car.hardware;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.GpsSatellite;
+import android.location.GpsStatus;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * CarSensorsProxy adds car sensors implementation for sensors that are not provided by vehicle HAL.
+ * @hide
+ */
+class CarSensorsProxy {
+ private static final String TAG = "CarSensorsProxy";
+ private static final int MSG_SENSORT_EVENT = 1;
+
+ // @GuardedBy("this")
+ private final Map<Integer, Set<CarSensorManager.CarSensorEventListener>> mListenersMultiMap;
+ private final LocationManager mLocationManager;
+ private final SensorManager mSensorManager;
+ private final Sensor mAccelerometerSensor;
+ private final Sensor mMagneticFieldSensor;
+ private final Sensor mGyroscopeSensor;
+ private final int[] mSupportedSensors;
+
+ // @GuardedBy("this")
+ private Location mLastLocation;
+ // @GuardedBy("this")
+ private GpsStatus mLastGpsStatus;
+ // @GuardedBy("this")
+ private float[] mLastAccelerometerData = new float[3];
+ // @GuardedBy("this")
+ private float[] mLastMagneticFieldData = new float[3];
+ // @GuardedBy("this")
+ private float[] mLastGyroscopeData = new float[3];
+ // @GuardedBy("this")
+ private float[] mR = new float[16];
+ // @GuardedBy("this")
+ private float[] mI = new float[16];
+ // @GuardedBy("this")
+ private float[] mOrientation = new float[3];
+ // @GuardedBy("this")
+ private long mLastLocationTime;
+ // @GuardedBy("this")
+ private long mLastGpsStatusTime;
+ // @GuardedBy("this")
+ private long mLastAccelerometerDataTime;
+ // @GuardedBy("this")
+ private long mLastMagneticFieldDataTime;
+ // @GuardedBy("this")
+ private long mLastGyroscopeDataTime;
+
+ private final GpsStatus.Listener mGpsStatusListener = new GpsStatus.Listener() {
+ @Override
+ public void onGpsStatusChanged(int event) {
+ if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
+ synchronized (CarSensorsProxy.this) {
+ mLastGpsStatus = mLocationManager.getGpsStatus(mLastGpsStatus);
+ mLastGpsStatusTime = System.nanoTime();
+ }
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE);
+ }
+ }
+ };
+
+ private final LocationListener mLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ synchronized (CarSensorsProxy.this) {
+ mLastLocation = location;
+ mLastLocationTime = System.nanoTime();
+ }
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_LOCATION);
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ };
+
+ private final SensorEventListener mSensorListener = new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ int type = event.sensor.getType();
+ synchronized (CarSensorsProxy.this) {
+ switch (type) {
+ case Sensor.TYPE_GYROSCOPE:
+ System.arraycopy(event.values, 0, mLastGyroscopeData, 0, 3);
+ mLastGyroscopeDataTime = System.nanoTime();
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_GYROSCOPE);
+ break;
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ System.arraycopy(event.values, 0, mLastMagneticFieldData, 0, 3);
+ mLastMagneticFieldDataTime = System.nanoTime();
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_COMPASS);
+ break;
+ case Sensor.TYPE_ACCELEROMETER:
+ System.arraycopy(event.values, 0, mLastAccelerometerData, 0, 3);
+ mLastAccelerometerDataTime = System.nanoTime();
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_ACCELEROMETER);
+ pushSensorChanges(CarSensorManager.SENSOR_TYPE_COMPASS);
+ break;
+ default:
+ Log.w(TAG, "Unexpected sensor event type: " + type);
+ // Should never happen.
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+ };
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SENSORT_EVENT:
+ int sensorType = msg.arg1;
+ Collection<CarSensorManager.CarSensorEventListener> listenersCollection;
+ synchronized (CarSensorsProxy.this) {
+ listenersCollection = mListenersMultiMap.get(sensorType);
+ }
+ CarSensorEvent event = (CarSensorEvent) msg.obj;
+ if (event != null) {
+ for (CarSensorManager.CarSensorEventListener listener :
+ listenersCollection) {
+ listener.onSensorChanged(event);
+ }
+ }
+ break;
+ default:
+ Log.w(TAG, "Unexpected msg dispatched. msg: " + msg);
+ super.handleMessage(msg);
+ }
+ }
+ };
+
+ CarSensorsProxy(Context context) {
+ mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ mMagneticFieldSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+ mGyroscopeSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ mListenersMultiMap = new HashMap<Integer, Set<CarSensorManager.CarSensorEventListener>>();
+ mSupportedSensors = initSupportedSensors(context);
+
+ }
+
+ private int[] initSupportedSensors(Context context) {
+ Set<Integer> features = new HashSet<>();
+ PackageManager packageManager = context.getPackageManager();
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_COMPASS)
+ && packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)) {
+ features.add(CarSensorManager.SENSOR_TYPE_COMPASS);
+ }
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)) {
+ features.add(CarSensorManager.SENSOR_TYPE_ACCELEROMETER);
+ }
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE)) {
+ features.add(CarSensorManager.SENSOR_TYPE_GYROSCOPE);
+ }
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION)) {
+ features.add(CarSensorManager.SENSOR_TYPE_LOCATION);
+ features.add(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE);
+ }
+ return toIntArray(features);
+ }
+
+ private static int[] toIntArray(Collection<Integer> collection) {
+ int len = collection.size();
+ int[] arr = new int[len];
+ int arrIndex = 0;
+ for (Integer item : collection) {
+ arr[arrIndex] = item;
+ arrIndex++;
+ }
+ return arr;
+ }
+
+
+ public boolean isSensorSupported(int sensorType) {
+ for (int sensor : mSupportedSensors) {
+ if (sensor == sensorType) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public int[] getSupportedSensors() {
+ return mSupportedSensors;
+ }
+
+ public boolean registerSensorListener(CarSensorManager.CarSensorEventListener listener,
+ int sensorType, int rate) {
+ // current implementation ignores rate.
+ boolean sensorSetChanged = false;
+ synchronized (this) {
+ if (mListenersMultiMap.get(sensorType) == null) {
+ mListenersMultiMap.put(sensorType,
+ new HashSet<CarSensorManager.CarSensorEventListener>());
+ sensorSetChanged = true;
+ }
+ mListenersMultiMap.get(sensorType).add(listener);
+ }
+ if (sensorSetChanged) {
+ updateSensorListeners();
+ }
+ return true;
+ }
+
+ public void unregisterSensorListener(CarSensorManager.CarSensorEventListener listener,
+ int sensorType) {
+ if (listener == null) {
+ return;
+ }
+ boolean sensorSetChanged = false;
+ synchronized (this) {
+ Set<CarSensorManager.CarSensorEventListener> sensorTypeListeneres =
+ mListenersMultiMap.get(sensorType);
+ if (sensorTypeListeneres != null) {
+ sensorTypeListeneres.remove(listener);
+ if (sensorTypeListeneres.isEmpty()) {
+ mListenersMultiMap.remove(sensorType);
+ sensorSetChanged = true;
+ }
+ }
+ }
+ if (sensorSetChanged) {
+ updateSensorListeners();
+ };
+ }
+
+ public void unregisterSensorListener(CarSensorManager.CarSensorEventListener listener) {
+ if (listener == null) {
+ return;
+ }
+ boolean sensorSetChanged = false;
+ synchronized (this) {
+ for (Map.Entry<Integer, Set<CarSensorManager.CarSensorEventListener>> entry :
+ mListenersMultiMap.entrySet()) {
+ if (entry.getValue().contains(listener)) {
+ entry.getValue().remove(listener);
+ }
+ if (entry.getValue().isEmpty()) {
+ sensorSetChanged = true;
+ mListenersMultiMap.remove(entry.getKey());
+ }
+ }
+ }
+ if (sensorSetChanged) {
+ updateSensorListeners();
+ };
+ }
+
+ public CarSensorEvent getLatestSensorEvent(int type) {
+ return getSensorEvent(type);
+ }
+
+ private void pushSensorChanges(int sensorType) {
+ CarSensorEvent event = getSensorEvent(sensorType);
+ if (event == null) {
+ return;
+ }
+ Message msg = mHandler.obtainMessage(MSG_SENSORT_EVENT, sensorType, 0, event);
+ mHandler.sendMessage(msg);
+ }
+
+ private CarSensorEvent getSensorEvent(int sensorType) {
+ CarSensorEvent event = null;
+ synchronized (this) {
+ switch (sensorType) {
+ case CarSensorManager.SENSOR_TYPE_COMPASS:
+ if (mLastMagneticFieldDataTime != 0 && mLastAccelerometerDataTime != 0) {
+ event = new CarSensorEvent(sensorType, Math.max(mLastMagneticFieldDataTime,
+ mLastAccelerometerDataTime), 3, 0);
+ SensorManager.getRotationMatrix(mR, mI, mLastAccelerometerData,
+ mLastMagneticFieldData);
+ SensorManager.getOrientation(mR, mOrientation);
+ event.floatValues[CarSensorEvent.INDEX_COMPASS_BEARING] =
+ (float) Math.toDegrees(mOrientation[0]);
+ event.floatValues[CarSensorEvent.INDEX_COMPASS_PITCH] =
+ (float) Math.toDegrees(mOrientation[1]);
+ event.floatValues[CarSensorEvent.INDEX_COMPASS_ROLL] =
+ (float) Math.toDegrees(mOrientation[2]);
+ }
+ break;
+ case CarSensorManager.SENSOR_TYPE_LOCATION:
+ if (mLastLocationTime != 0) {
+ event = new CarSensorEvent(sensorType, mLastLocationTime, 6, 3);
+ populateLocationCarSensorEvent(event, mLastLocation);
+ }
+ break;
+ case CarSensorManager.SENSOR_TYPE_ACCELEROMETER:
+ if (mLastAccelerometerDataTime != 0) {
+ event = new CarSensorEvent(sensorType, mLastAccelerometerDataTime, 3, 0);
+ event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_X] =
+ mLastAccelerometerData[0];
+ event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_Y] =
+ mLastAccelerometerData[1];
+ event.floatValues[CarSensorEvent.INDEX_ACCELEROMETER_Z] =
+ mLastAccelerometerData[2];
+ }
+ break;
+ case CarSensorManager.SENSOR_TYPE_GPS_SATELLITE:
+ if (mLastGpsStatusTime != 0) {
+ event = createGpsStatusCarSensorEvent(mLastGpsStatus);
+ }
+ break;
+ case CarSensorManager.SENSOR_TYPE_GYROSCOPE:
+ if (mLastGyroscopeDataTime != 0) {
+ event = new CarSensorEvent(sensorType, mLastGyroscopeDataTime, 3, 0);
+ event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_X] = mLastGyroscopeData[0];
+ event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_Y] = mLastGyroscopeData[1];
+ event.floatValues[CarSensorEvent.INDEX_GYROSCOPE_Z] = mLastGyroscopeData[2];
+ }
+ break;
+ default:
+ // Should not happen.
+ Log.w(TAG, "[getSensorEvent]: Unsupported sensor type:" + sensorType);
+ return null;
+ }
+ }
+ return event;
+ }
+
+ private void populateLocationCarSensorEvent(CarSensorEvent event, Location location) {
+ if (location == null) {
+ return;
+ }
+ int present = 0;
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_LONGITUDE);
+ event.intValues[CarSensorEvent.INDEX_LOCATION_LATITUDE_INTS] =
+ (int) (location.getLongitude() * 1E7);
+
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_LATITUDE);
+ event.intValues[CarSensorEvent.INDEX_LOCATION_LATITUDE_INTS] =
+ (int) (location.getLatitude() * 1E7);
+
+ if (location.hasAccuracy()) {
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_ACCURACY);
+ event.floatValues[CarSensorEvent.INDEX_LOCATION_ACCURACY] = location.getAccuracy();
+ }
+
+ if (location.hasAltitude()) {
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_ALTITUDE);
+ event.floatValues[CarSensorEvent.INDEX_LOCATION_ALTITUDE] =
+ (float) location.getAltitude();
+ }
+
+ if (location.hasSpeed()) {
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_SPEED);
+ event.floatValues[CarSensorEvent.INDEX_LOCATION_SPEED] = location.getSpeed();
+ }
+
+ if (location.hasBearing()) {
+ present |= (0x1 << CarSensorEvent.INDEX_LOCATION_BEARING);
+ event.floatValues[CarSensorEvent.INDEX_LOCATION_BEARING] = location.getBearing();
+ }
+
+ event.intValues[0] = present;
+ }
+
+ private CarSensorEvent createGpsStatusCarSensorEvent(GpsStatus gpsStatus) {
+ CarSensorEvent event = null;
+
+ if (gpsStatus == null) {
+ return event;
+ }
+
+ int numberInView = 0;
+ int numberInUse = 0;
+ for (GpsSatellite satellite : gpsStatus.getSatellites()) {
+ ++numberInView;
+ if (satellite.usedInFix()) {
+ ++numberInUse;
+ }
+ }
+ int floatValuesSize = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_INTERVAL * numberInView
+ + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_OFFSET;
+ int intValuesSize = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_INTERVAL * numberInView
+ + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_OFFSET;
+ event = new CarSensorEvent(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE, mLastGpsStatusTime,
+ floatValuesSize, intValuesSize);
+ event.intValues[CarSensorEvent.INDEX_GPS_SATELLITE_NUMBER_IN_USE] = numberInUse;
+ event.intValues[CarSensorEvent.INDEX_GPS_SATELLITE_NUMBER_IN_VIEW] = numberInView;
+ int i = 0;
+ for (GpsSatellite satellite : gpsStatus.getSatellites()) {
+ int iInt = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_OFFSET
+ + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_INT_INTERVAL * i;
+ int iFloat = CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_OFFSET
+ + CarSensorEvent.INDEX_GPS_SATELLITE_ARRAY_FLOAT_INTERVAL * i;
+ event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_PRN_OFFSET] =
+ satellite.getPrn();
+ event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_SNR_OFFSET] =
+ satellite.getSnr();
+ event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_AZIMUTH_OFFSET] =
+ satellite.getAzimuth();
+ event.floatValues[iFloat + CarSensorEvent.INDEX_GPS_SATELLITE_ELEVATION_OFFSET] =
+ satellite.getElevation();
+ event.intValues[iInt] = satellite.usedInFix() ? 1 : 0;
+ i++;
+ }
+ return event;
+ }
+
+ private void updateSensorListeners() {
+ Set<Integer> activeSensors;
+ synchronized (this) {
+ activeSensors = mListenersMultiMap.keySet();
+ }
+ if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_LOCATION)) {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
+ mLocationListener);
+ } else {
+ mLocationManager.removeUpdates(mLocationListener);
+ }
+
+ if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_GPS_SATELLITE)) {
+ mLocationManager.addGpsStatusListener(mGpsStatusListener);
+ } else {
+ mLocationManager.removeGpsStatusListener(mGpsStatusListener);
+ }
+
+ if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_ACCELEROMETER)
+ || activeSensors.contains(CarSensorManager.SENSOR_TYPE_COMPASS)) {
+ mSensorManager.registerListener(mSensorListener, mAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener, mAccelerometerSensor);
+ }
+
+ if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_COMPASS)) {
+ mSensorManager.registerListener(mSensorListener, mMagneticFieldSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener, mMagneticFieldSensor);
+ }
+
+ if (activeSensors.contains(CarSensorManager.SENSOR_TYPE_GYROSCOPE)) {
+ mSensorManager.registerListener(mSensorListener, mGyroscopeSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener, mGyroscopeSensor);
+ }
+ }
+}
diff --git a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
index 003451f..de792ed 100644
--- a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
+++ b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkConsts.java
@@ -407,6 +407,16 @@
}
}
+public static class VehicleAudioVolumeCapabilityFlag {
+public static final int VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE = 0x1;
+public static String enumToString(int v) {
+switch(v) {
+case VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE: return "VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE";
+default: return "UNKNOWN";
+}
+}
+}
+
public static class VehicleAudioVolumeState {
public static final int VEHICLE_AUDIO_VOLUME_STATE_OK = 0;
public static final int VEHICLE_AUDIO_VOLUME_STATE_LIMIT_REACHED = 1;
@@ -458,10 +468,10 @@
}
public static class VehicleAudioHwVariantConfigFlag {
-public static final int VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG = 0x1;
+public static final int VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG = 0x1;
public static String enumToString(int v) {
switch(v) {
-case VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG: return "VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG";
+case VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG: return "VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG";
default: return "UNKNOWN";
}
}
diff --git a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
index 5dc5a6f..ac2f393 100644
--- a/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
+++ b/libvehiclenetwork/java/src/com/android/car/vehiclenetwork/VehicleNetworkProtoUtil.java
@@ -18,6 +18,8 @@
import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
+import java.util.Arrays;
+
public class VehicleNetworkProtoUtil {
public static String VehiclePropValueToString(VehiclePropValue value) {
StringBuilder sb = new StringBuilder();
@@ -25,6 +27,8 @@
sb.append(Integer.toHexString(value.getProp()));
sb.append(" type:0x");
sb.append(Integer.toHexString(value.getValueType()));
+ sb.append(" floatValues:" + Arrays.toString(value.getFloatValuesList().toArray()));
+ sb.append(" integerValues:" + Arrays.toString(value.getInt32ValuesList().toArray()));
return sb.toString();
}
diff --git a/service/jni/Android.mk b/service/jni/Android.mk
index ed29126..012ddfa 100644
--- a/service/jni/Android.mk
+++ b/service/jni/Android.mk
@@ -32,6 +32,7 @@
LOCAL_CFLAGS := \
-Wno-unused-parameter \
+ -std=c++11
LOCAL_MODULE := libjni_car_service
LOCAL_MODULE_TAGS := optional
diff --git a/service/jni/com_android_car_CarInputService.cpp b/service/jni/com_android_car_CarInputService.cpp
index 1b02c12..a45d7e6 100644
--- a/service/jni/com_android_car_CarInputService.cpp
+++ b/service/jni/com_android_car_CarInputService.cpp
@@ -25,30 +25,42 @@
#include <android/keycodes.h>
#include <cutils/log.h>
#include <utils/Errors.h>
-
+#include <unordered_map>
namespace android {
static int androidKeyCodeToLinuxKeyCode(int androidKeyCode) {
- switch (androidKeyCode) {
- case AKEYCODE_VOLUME_UP:
- return KEY_VOLUMEUP;
- case AKEYCODE_VOLUME_DOWN:
- return KEY_VOLUMEDOWN;
- case AKEYCODE_CALL:
- return KEY_SEND;
- case AKEYCODE_ENDCALL:
- return KEY_END;
- /* TODO add more keys like these:
- case AKEYCODE_MEDIA_PLAY_PAUSE:
- case AKEYCODE_MEDIA_STOP:
- case AKEYCODE_MEDIA_NEXT:
- case AKEYCODE_MEDIA_PREVIOUS:*/
- case AKEYCODE_VOICE_ASSIST:
- return KEY_MICMUTE;
- default:
+ // Map Android Key Code to Linux Kernel codes
+ // according to frameworks/base/data/keyboards/Generic.kl
+
+ static const std::unordered_map<int, int> key_map {
+ { AKEYCODE_VOLUME_UP, KEY_VOLUMEUP },
+ { AKEYCODE_VOLUME_DOWN, KEY_VOLUMEDOWN },
+ { AKEYCODE_VOLUME_MUTE, KEY_MUTE },
+ { AKEYCODE_CALL, KEY_PHONE },
+ { AKEYCODE_ENDCALL, KEY_END }, // Currently not supported in Generic.kl
+ { AKEYCODE_MUSIC, KEY_SOUND },
+ { AKEYCODE_MEDIA_PLAY_PAUSE, KEY_PLAYPAUSE },
+ { AKEYCODE_MEDIA_PLAY, KEY_PLAY },
+ { AKEYCODE_BREAK, KEY_PAUSE },
+ { AKEYCODE_MEDIA_STOP, KEY_STOP },
+ { AKEYCODE_MEDIA_FAST_FORWARD, KEY_FASTFORWARD },
+ { AKEYCODE_MEDIA_REWIND, KEY_REWIND },
+ { AKEYCODE_MEDIA_NEXT, KEY_NEXTSONG },
+ { AKEYCODE_MEDIA_PREVIOUS, KEY_PREVIOUSSONG },
+ { AKEYCODE_CHANNEL_UP, KEY_CHANNELUP },
+ { AKEYCODE_CHANNEL_DOWN, KEY_CHANNELDOWN },
+ { AKEYCODE_VOICE_ASSIST, KEY_MICMUTE },
+ { AKEYCODE_HOME, KEY_HOME }
+ };
+
+ std::unordered_map<int, int>::const_iterator got = key_map.find(androidKeyCode);
+
+ if (got == key_map.end()) {
ALOGW("Unmapped android key code %d dropped", androidKeyCode);
return 0;
+ } else {
+ return got->second;
}
}
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 24ef730..c722932 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -33,15 +33,18 @@
There is no "radio" as radio routing is outside android (for external module) or same as
music (for android internal module)
OEM can put multiple policies as item and VEHICLE_PROPERTY_AUDIO_HW_VARIANT in vehicle HAL
- can decide which policy to use for the given H/W. This allows OEMs to support multuple
+ can decide which policy to use for the given H/W. This allows OEMs to support multiple
audio policy from single android S/W by detecting system's audio capability in
vehicle HAL.-->
<string-array translatable="false" name="audioRoutingPolicy">
<!-- alll logical streams into single physical stream 0. -->
- <item>"0:call,media,nav_guidance,voice_command,alarm,notification,system,safety,unknown"</item>
+ <item>"0:call,media,radio,nav_guidance,voice_command,alarm,notification,system,safety,unknown"</item>
<!-- call and media to physical stream 0 while all others go to physical stream 1 -->
- <item>"0:call,media,unknown#1:nav_guidance,voice_command,alarm,notification,system,safety"</item>
+ <item>"0:call,media,radio,unknown#1:nav_guidance,voice_command,alarm,notification,system,safety"</item>
</string-array>
+ <!-- Timeout value in Ms for audio focus wait. Audio focus request not responsed within
+ this value will be treated as timeout and audio focus will be reset to LOSS state. -->
+ <integer name="audioFocusWaitTimeoutMs">1000</integer>
<!-- This is kernel device node to allow input event injection for key inputs coming
from vehicle hal -->
diff --git a/service/src/com/android/car/AudioRoutingPolicy.java b/service/src/com/android/car/AudioRoutingPolicy.java
index 39ad826..fdad5aa 100644
--- a/service/src/com/android/car/AudioRoutingPolicy.java
+++ b/service/src/com/android/car/AudioRoutingPolicy.java
@@ -44,13 +44,13 @@
}
private static int getStreamType(String str) {
- // no radio here as radio routing is outside android (for external module) or same as music
- // (for android internal module)
switch (str) {
case "call":
return CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL;
case "media":
return CarAudioManager.CAR_AUDIO_USAGE_MUSIC;
+ case "radio":
+ return CarAudioManager.CAR_AUDIO_USAGE_RADIO;
case "nav_guidance":
return CarAudioManager.CAR_AUDIO_USAGE_NAVIGATION_GUIDANCE;
case "voice_command":
@@ -101,20 +101,8 @@
}
for (int i = 0; i < mPhysicalStreamForLogicalStream.length; i++) {
if (mPhysicalStreamForLogicalStream[i] == USAGE_TYPE_INVALID) {
- if (i == CarAudioManager.CAR_AUDIO_USAGE_RADIO) {
- // set radio routing to be the same as music. For external radio, this does not
- // matter. For internal one, it should be the same as music.
- int musicPhysicalStream =
- mPhysicalStreamForLogicalStream[CarAudioManager.CAR_AUDIO_USAGE_MUSIC];
- if (musicPhysicalStream == USAGE_TYPE_INVALID) {
- musicPhysicalStream = defaultStreamType;
- }
- mPhysicalStreamForLogicalStream[i] = musicPhysicalStream;
- } else {
- Log.w(CarLog.TAG_AUDIO, "Audio routing policy did not cover logical stream " +
- i);
- mPhysicalStreamForLogicalStream[i] = defaultStreamType;
- }
+ Log.w(CarLog.TAG_AUDIO, "Audio routing policy did not cover logical stream " + i);
+ mPhysicalStreamForLogicalStream[i] = defaultStreamType;
}
}
}
diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java
index 5019fb4..2632c02 100644
--- a/service/src/com/android/car/CarAudioService.java
+++ b/service/src/com/android/car/CarAudioService.java
@@ -18,6 +18,7 @@
import android.car.media.CarAudioManager;
import android.car.media.ICarAudio;
import android.content.Context;
+import android.content.res.Resources;
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
@@ -30,7 +31,7 @@
import android.util.Log;
import com.android.car.hal.AudioHalService;
-import com.android.car.hal.AudioHalService.AudioHalListener;
+import com.android.car.hal.AudioHalService.AudioHalFocusListener;
import com.android.car.hal.VehicleHal;
import com.android.internal.annotations.GuardedBy;
@@ -38,9 +39,19 @@
import java.util.LinkedList;
-public class CarAudioService extends ICarAudio.Stub implements CarServiceBase, AudioHalListener {
+public class CarAudioService extends ICarAudio.Stub implements CarServiceBase,
+ AudioHalFocusListener {
- private static final long FOCUS_RESPONSE_WAIT_TIMEOUT_MS = 1000;
+ public interface AudioContextChangeListener {
+ /**
+ * Notifies the current primary audio context (app holding focus).
+ * If there is no active context, context will be 0.
+ * Will use context like CarAudioManager.CAR_AUDIO_USAGE_*
+ */
+ void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream);
+ }
+
+ private final long mFocusResponseWaitTimeoutMs;
private static final String TAG_FOCUS = CarLog.TAG_AUDIO + ".FOCUS";
@@ -50,11 +61,12 @@
private final Context mContext;
private final HandlerThread mFocusHandlerThread;
private final CarAudioFocusChangeHandler mFocusHandler;
- private final CarAudioVolumeHandler mVolumeHandler;
private final SystemFocusListener mSystemFocusListener;
- private AudioPolicy mAudioPolicy;
+
private final Object mLock = new Object();
@GuardedBy("mLock")
+ private AudioPolicy mAudioPolicy;
+ @GuardedBy("mLock")
private FocusState mCurrentFocusState = FocusState.STATE_LOSS;
/** Focus state received, but not handled yet. Once handled, this will be set to null. */
@GuardedBy("mLock")
@@ -83,6 +95,16 @@
private boolean mCallActive = false;
@GuardedBy("mLock")
private int mCurrentAudioContexts = 0;
+ @GuardedBy("mLock")
+ private int mCurrentPrimaryAudioContext = 0;
+ @GuardedBy("mLock")
+ private int mCurrentPrimaryPhysicalStream = 0;
+ @GuardedBy("mLock")
+ private AudioContextChangeListener mAudioContextChangeListener;
+ @GuardedBy("mLock")
+ private CarAudioContextChangeHandler mCarAudioContextChangeHandler;
+ @GuardedBy("mLock")
+ private boolean mIsRadioExternal;
private final AudioAttributes mAttributeBottom =
CarAudioAttributesUtil.getAudioAttributesForCarUsage(
@@ -98,8 +120,10 @@
mSystemFocusListener = new SystemFocusListener();
mFocusHandlerThread.start();
mFocusHandler = new CarAudioFocusChangeHandler(mFocusHandlerThread.getLooper());
- mVolumeHandler = new CarAudioVolumeHandler(Looper.getMainLooper());
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ Resources res = context.getResources();
+ mFocusResponseWaitTimeoutMs = (long) res.getInteger(R.integer.audioFocusWaitTimeoutMs);
+
}
@Override
@@ -115,7 +139,9 @@
if (isFocusSuported) {
builder.setAudioPolicyFocusListener(mSystemFocusListener);
}
- mAudioPolicy = builder.build();
+ synchronized (mLock) {
+ mAudioPolicy = builder.build();
+ }
if (isFocusSuported) {
FocusState currentState = FocusState.create(mAudioHal.getCurrentFocusState());
int r = mAudioManager.requestAudioFocus(mBottomAudioFocusHandler, mAttributeBottom,
@@ -134,9 +160,12 @@
if (r != 0) {
throw new RuntimeException("registerAudioPolicy failed " + r);
}
- mAudioHal.setListener(this);
+ mAudioHal.setFocusListener(this);
int audioHwVariant = mAudioHal.getHwVariant();
- mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
+ synchronized (mLock) {
+ mAudioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant);
+ mIsRadioExternal = mAudioHal.isRadioExternal();
+ }
mAudioHal.setAudioRoutingPolicy(mAudioRoutingPolicy);
//TODO set routing policy with new AudioPolicy API. This will control which logical stream
// goes to which physical stream.
@@ -154,9 +183,27 @@
mTopFocusInfo = null;
mPendingFocusChanges.clear();
mRadioActive = false;
+ if (mCarAudioContextChangeHandler != null) {
+ mCarAudioContextChangeHandler.cancelAll();
+ mCarAudioContextChangeHandler = null;
+ }
+ mAudioContextChangeListener = null;
+ mCurrentPrimaryAudioContext = 0;
}
}
+ public synchronized void setAudioContextChangeListener(Looper looper,
+ AudioContextChangeListener listener) {
+ if (looper == null || listener == null) {
+ throw new IllegalArgumentException("looper or listener null");
+ }
+ if (mCarAudioContextChangeHandler != null) {
+ mCarAudioContextChangeHandler.cancelAll();
+ }
+ mCarAudioContextChangeHandler = new CarAudioContextChangeHandler(looper);
+ mAudioContextChangeListener = listener;
+ }
+
@Override
public void dump(PrintWriter writer) {
writer.println("*CarAudioService*");
@@ -164,6 +211,9 @@
" mLastFocusRequestToCar:" + mLastFocusRequestToCar);
writer.println(" mCurrentAudioContexts:0x" + Integer.toHexString(mCurrentAudioContexts));
writer.println(" mCallActive:" + mCallActive + " mRadioActive:" + mRadioActive);
+ writer.println(" mCurrentPrimaryAudioContext:" + mCurrentPrimaryAudioContext +
+ " mCurrentPrimaryPhysicalStream:" + mCurrentPrimaryPhysicalStream);
+ writer.println(" mIsRadioExternal:" + mIsRadioExternal);
mAudioRoutingPolicy.dump(writer);
}
@@ -178,17 +228,6 @@
}
@Override
- public void onVolumeChange(int streamNumber, int volume, int volumeState) {
- mVolumeHandler.handleVolumeChange(new VolumeStateChangeEvent(streamNumber, volume,
- volumeState));
- }
-
- @Override
- public void onVolumeLimitChange(int streamNumber, int volume) {
- //TODO
- }
-
- @Override
public void onStreamStatusChange(int state, int streamNumber) {
mFocusHandler.handleStreamStateChange(state, streamNumber);
}
@@ -333,10 +372,6 @@
androidFocus, flags, mAudioPolicy);
}
- private void doHandleVolumeChange(VolumeStateChangeEvent event) {
- //TODO
- }
-
private void doHandleStreamStatusChange(int streamNumber, int state) {
//TODO
}
@@ -368,8 +403,8 @@
return false;
}
- private boolean isFocusFromRadio(AudioFocusInfo info) {
- if (!mAudioHal.isRadioExternal()) {
+ private boolean isFocusFromExternalRadio(AudioFocusInfo info) {
+ if (!mIsRadioExternal) {
// if radio is not external, no special handling of radio is necessary.
return false;
}
@@ -430,6 +465,24 @@
int logicalStreamTypeForTop = CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib);
int physicalStreamTypeForTop = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
logicalStreamTypeForTop);
+
+ // update primary context and notify if necessary
+ int primaryContext = logicalStreamTypeForTop;
+ switch (logicalStreamTypeForTop) {
+ case CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_BOTTOM:
+ case CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY:
+ primaryContext = 0;
+ break;
+ }
+ if (mCurrentPrimaryAudioContext != primaryContext) {
+ mCurrentPrimaryAudioContext = primaryContext;
+ mCurrentPrimaryPhysicalStream = physicalStreamTypeForTop;
+ if (mCarAudioContextChangeHandler != null) {
+ mCarAudioContextChangeHandler.requestContextChangeNotification(
+ mAudioContextChangeListener, primaryContext, physicalStreamTypeForTop);
+ }
+ }
+
int audioContexts = 0;
if (logicalStreamTypeForTop == CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL) {
if (!mCallActive) {
@@ -440,7 +493,8 @@
if (mCallActive) {
mCallActive = false;
}
- audioContexts = AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
+ audioContexts =
+ AudioHalService.logicalStreamToHalContextType(logicalStreamTypeForTop);
}
// other apps having focus
int focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE;
@@ -448,10 +502,8 @@
int streamsToRequest = 0x1 << physicalStreamTypeForTop;
switch (mTopFocusInfo.getGainRequest()) {
case AudioManager.AUDIOFOCUS_GAIN:
- if (isFocusFromRadio(mTopFocusInfo)) {
+ if (isFocusFromExternalRadio(mTopFocusInfo)) {
mRadioActive = true;
- // audio context sending is only for audio from android.
- audioContexts = 0;
} else {
mRadioActive = false;
}
@@ -494,9 +546,9 @@
// Most cars do not allow that, but if mixing is possible, it can take media stream.
// For now, assume no mixing capability.
int radioPhysicalStream = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream(
- CarAudioManager.CAR_AUDIO_USAGE_MUSIC);
- if (!isFocusFromRadio(mTopFocusInfo) &&
- (physicalStreamTypeForTop == radioPhysicalStream)) {
+ CarAudioManager.CAR_AUDIO_USAGE_RADIO);
+ if (!isFocusFromExternalRadio(mTopFocusInfo) &&
+ (physicalStreamTypeForTop == radioPhysicalStream) && mIsRadioExternal) {
Log.i(CarLog.TAG_AUDIO, "Top stream is taking the same stream:" +
physicalStreamTypeForTop + " as radio, stopping radio");
// stream conflict here. radio cannot be played
@@ -542,7 +594,7 @@
mAudioHal.requestAudioFocusChange(focusToRequest, streamsToRequest, extFocus,
audioContexts);
try {
- mLock.wait(FOCUS_RESPONSE_WAIT_TIMEOUT_MS);
+ mLock.wait(mFocusResponseWaitTimeoutMs);
} catch (InterruptedException e) {
//ignore
}
@@ -657,7 +709,7 @@
mAudioHal.requestAudioFocusChange(
AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE, 0, 0);
try {
- mLock.wait(FOCUS_RESPONSE_WAIT_TIMEOUT_MS);
+ mLock.wait(mFocusResponseWaitTimeoutMs);
} catch (InterruptedException e) {
//ignore
}
@@ -748,6 +800,37 @@
}
}
+ private class CarAudioContextChangeHandler extends Handler {
+ private static final int MSG_CONTEXT_CHANGE = 0;
+
+ private CarAudioContextChangeHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void requestContextChangeNotification(AudioContextChangeListener listener,
+ int primaryContext, int physicalStream) {
+ Message msg = obtainMessage(MSG_CONTEXT_CHANGE, primaryContext, physicalStream,
+ listener);
+ sendMessage(msg);
+ }
+
+ private void cancelAll() {
+ removeMessages(MSG_CONTEXT_CHANGE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CONTEXT_CHANGE: {
+ AudioContextChangeListener listener = (AudioContextChangeListener) msg.obj;
+ int context = msg.arg1;
+ int physicalStream = msg.arg2;
+ listener.onContextChange(context, physicalStream);
+ } break;
+ }
+ }
+ }
+
private class CarAudioFocusChangeHandler extends Handler {
private static final int MSG_FOCUS_CHANGE = 0;
private static final int MSG_STREAM_STATE_CHANGE = 1;
@@ -815,40 +898,6 @@
}
}
- private class CarAudioVolumeHandler extends Handler {
- private static final int MSG_VOLUME_CHANGE = 0;
-
- private CarAudioVolumeHandler(Looper looper) {
- super(looper);
- }
-
- private void handleVolumeChange(VolumeStateChangeEvent event) {
- Message msg = obtainMessage(MSG_VOLUME_CHANGE, event);
- sendMessage(msg);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_VOLUME_CHANGE:
- doHandleVolumeChange((VolumeStateChangeEvent) msg.obj);
- break;
- }
- }
- }
-
- private static class VolumeStateChangeEvent {
- public final int stream;
- public final int volume;
- public final int state;
-
- public VolumeStateChangeEvent(int stream, int volume, int state) {
- this.stream = stream;
- this.volume = volume;
- this.state = state;
- }
- }
-
/** Wrapper class for holding the current focus state from car. */
private static class FocusState {
public final int focusState;
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 8b0bd3d..6340549 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -42,11 +42,13 @@
private final Context mContext;
- private KeyEventListener mVoiceAssitantKeyListener;
- private KeyEventListener mLongVoiceAssitantKeyListener;
+ private KeyEventListener mVoiceAssistantKeyListener;
+ private KeyEventListener mLongVoiceAssistantKeyListener;
private long mLastVoiceKeyDownTime = 0;
- private KeyEventListener mInstumentClusterKeyListener;
+ private KeyEventListener mInstrumentClusterKeyListener;
+
+ private KeyEventListener mVolumeKeyListener;
private ParcelFileDescriptor mInjectionDeviceFd;
@@ -62,9 +64,9 @@
* If listener is set, short key press will lead into calling the listener.
* @param listener
*/
- public void setVoiceAssitantKeyListener(KeyEventListener listener) {
+ public void setVoiceAssistantKeyListener(KeyEventListener listener) {
synchronized (this) {
- mVoiceAssitantKeyListener = listener;
+ mVoiceAssistantKeyListener = listener;
}
}
@@ -74,15 +76,21 @@
* If listener is set, short long press will lead into calling the listener.
* @param listener
*/
- public void setLongVoiceAssitantKeyListener(KeyEventListener listener) {
+ public void setLongVoiceAssistantKeyListener(KeyEventListener listener) {
synchronized (this) {
- mLongVoiceAssitantKeyListener = listener;
+ mLongVoiceAssistantKeyListener = listener;
}
}
public void setInstrumentClusterKeyListener(KeyEventListener listener) {
synchronized (this) {
- mInstumentClusterKeyListener = listener;
+ mInstrumentClusterKeyListener = listener;
+ }
+ }
+
+ public void setVolumeKeyListener(KeyEventListener listener) {
+ synchronized (this) {
+ mVolumeKeyListener = listener;
}
}
@@ -112,9 +120,9 @@
@Override
public void release() {
synchronized (this) {
- mVoiceAssitantKeyListener = null;
- mLongVoiceAssitantKeyListener = null;
- mInstumentClusterKeyListener = null;
+ mVoiceAssistantKeyListener = null;
+ mLongVoiceAssistantKeyListener = null;
+ mInstrumentClusterKeyListener = null;
if (mInjectionDeviceFd != null) {
try {
mInjectionDeviceFd.close();
@@ -136,6 +144,10 @@
case KeyEvent.KEYCODE_VOICE_ASSIST:
handleVoiceAssistKey(event);
return;
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ handleVolumeKey(event);
+ return;
default:
break;
}
@@ -160,12 +172,12 @@
KeyEventListener longPressListener = null;
long downTime;
synchronized (this) {
- shortPressListener = mVoiceAssitantKeyListener;
- longPressListener = mLongVoiceAssitantKeyListener;
+ shortPressListener = mVoiceAssistantKeyListener;
+ longPressListener = mLongVoiceAssistantKeyListener;
downTime = mLastVoiceKeyDownTime;
}
if (shortPressListener == null && longPressListener == null) {
- launchDefaultVoiceAssitantHandler();
+ launchDefaultVoiceAssistantHandler();
} else {
long duration = SystemClock.elapsedRealtime() - downTime;
listener = (duration > VOICE_LONG_PRESS_TIME_MS
@@ -173,13 +185,13 @@
if (listener != null) {
listener.onKeyEvent(event);
} else {
- launchDefaultVoiceAssitantHandler();
+ launchDefaultVoiceAssistantHandler();
}
}
}
}
- private void launchDefaultVoiceAssitantHandler() {
+ private void launchDefaultVoiceAssistantHandler() {
Log.i(CarLog.TAG_INPUT, "voice key, launch default intent");
Intent voiceIntent =
new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
@@ -189,7 +201,18 @@
private void handleInstrumentClusterKey(KeyEvent event) {
KeyEventListener listener = null;
synchronized (this) {
- listener = mInstumentClusterKeyListener;
+ listener = mInstrumentClusterKeyListener;
+ }
+ if (listener == null) {
+ return;
+ }
+ listener.onKeyEvent(event);
+ }
+
+ private void handleVolumeKey(KeyEvent event) {
+ KeyEventListener listener = null;
+ synchronized (this) {
+ listener = mVolumeKeyListener;
}
if (listener == null) {
return;
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 66c75fa..35e6494 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -23,10 +23,7 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
@@ -184,9 +181,9 @@
CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH);
}
}
- mCarInputService.setVoiceAssitantKeyListener(listenShortPress
+ mCarInputService.setVoiceAssistantKeyListener(listenShortPress
? mVoiceAssistantKeyListener : null);
- mCarInputService.setLongVoiceAssitantKeyListener(listenLongPress
+ mCarInputService.setLongVoiceAssistantKeyListener(listenLongPress
? mLongVoiceAssistantKeyListener : null);
}
diff --git a/service/src/com/android/car/hal/AudioHalService.java b/service/src/com/android/car/hal/AudioHalService.java
index 893fbff..d9906cd 100644
--- a/service/src/com/android/car/hal/AudioHalService.java
+++ b/service/src/com/android/car/hal/AudioHalService.java
@@ -33,7 +33,9 @@
import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioRoutingPolicyIndex;
import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamState;
import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioStreamStateIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeCapabilityFlag;
import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeIndex;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleAudioVolumeLimitIndex;
import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfigs;
import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
@@ -126,7 +128,7 @@
public static final int AUDIO_CONTEXT_SYSTEM_SOUND_FLAG =
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_SYSTEM_SOUND_FLAG;
- public interface AudioHalListener {
+ public interface AudioHalFocusListener {
/**
* Audio focus change from car.
* @param focusState
@@ -136,6 +138,15 @@
*/
void onFocusChange(int focusState, int streams, int externalFocus);
/**
+ * Stream state change (start / stop) from android
+ * @param streamNumber
+ * @param state
+ */
+ void onStreamStatusChange(int streamNumber, int state);
+ }
+
+ public interface AudioHalVolumeListener {
+ /**
* Audio volume change from car.
* @param streamNumber
* @param volume
@@ -148,38 +159,25 @@
* @param volume
*/
void onVolumeLimitChange(int streamNumber, int volume);
- /**
- * Stream state change (start / stop) from android
- * @param streamNumber
- * @param state
- */
- void onStreamStatusChange(int streamNumber, int state);
}
private final VehicleHal mVehicleHal;
- private AudioHalListener mListener;
+ private AudioHalFocusListener mFocusListener;
+ private AudioHalVolumeListener mVolumeListener;
private int mVariant;
- private List<VehiclePropValue> mQueuedEvents;
-
private final HashMap<Integer, VehiclePropConfig> mProperties = new HashMap<>();
public AudioHalService(VehicleHal vehicleHal) {
mVehicleHal = vehicleHal;
}
- public void setListener(AudioHalListener listener) {
- List<VehiclePropValue> eventsToDispatch = null;
- synchronized (this) {
- mListener = listener;
- if (mQueuedEvents != null) {
- eventsToDispatch = mQueuedEvents;
- mQueuedEvents = null;
- }
- }
- if (eventsToDispatch != null) {
- dispatchEventToListener(listener, eventsToDispatch);
- }
+ public synchronized void setFocusListener(AudioHalFocusListener focusListener) {
+ mFocusListener = focusListener;
+ }
+
+ public synchronized void setVolumeListener(AudioHalVolumeListener volumeListener) {
+ mVolumeListener = volumeListener;
}
public void setAudioRoutingPolicy(AudioRoutingPolicy policy) {
@@ -262,7 +260,7 @@
return true;
}
return (config.getConfigArray(0) &
- VehicleAudioHwVariantConfigFlag.VEHICLE_AUDIO_HW_VARIANT_FLAG_PASS_RADIO_AUDIO_FOCUS_FLAG)
+ VehicleAudioHwVariantConfigFlag.VEHICLE_AUDIO_HW_VARIANT_FLAG_INTERNAL_RADIO_FLAG)
== 0;
}
@@ -270,6 +268,44 @@
return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS);
}
+ public synchronized boolean isAudioVolumeSupported() {
+ return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+ }
+
+ public synchronized int getSupportedAudioVolumeContexts() {
+ if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+ throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+ }
+ VehiclePropConfig config = mProperties.get(
+ VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+ return config.getConfigArray(0);
+ }
+
+ /**
+ * Whether external audio module can memorize logical audio volumes or not.
+ * @return
+ */
+ public synchronized boolean isExternalAudioVolumePersistent() {
+ if (!isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME)) {
+ throw new IllegalStateException("VEHICLE_PROPERTY_AUDIO_VOLUME not supported");
+ }
+ VehiclePropConfig config = mProperties.get(
+ VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME);
+ if (config.getConfigArray(0) == 0) { // physical streams only
+ return false;
+ }
+ if ((config.getConfigArray(1) &
+ VehicleAudioVolumeCapabilityFlag.VEHICLE_AUDIO_VOLUME_CAPABILITY_PERSISTENT_STORAGE)
+ != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized boolean isAudioVolumeLimitSupported() {
+ return isPropertySupportedLocked(VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT);
+ }
+
/**
* Get the current audio focus state.
* @return 0: focusState, 1: streams, 2: externalFocus
@@ -340,22 +376,18 @@
@Override
public void handleHalEvents(List<VehiclePropValue> values) {
- AudioHalListener listener = null;
+ AudioHalFocusListener focusListener = null;
+ AudioHalVolumeListener volumeListener = null;
synchronized (this) {
- listener = mListener;
- if (listener == null) {
- if (mQueuedEvents == null) {
- mQueuedEvents = new LinkedList<VehiclePropValue>();
- }
- mQueuedEvents.addAll(values);
- }
+ focusListener = mFocusListener;
+ volumeListener = mVolumeListener;
}
- if (listener != null) {
- dispatchEventToListener(listener, values);
- }
+ dispatchEventToListener(focusListener, volumeListener, values);
}
- private void dispatchEventToListener(AudioHalListener listener, List<VehiclePropValue> values) {
+ private void dispatchEventToListener(AudioHalFocusListener focusListener,
+ AudioHalVolumeListener volumeListener,
+ List<VehiclePropValue> values) {
for (VehiclePropValue v : values) {
switch (v.getProp()) {
case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_FOCUS: {
@@ -365,7 +397,18 @@
VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_STREAMS);
int externalFocus = v.getInt32Values(
VehicleAudioFocusIndex.VEHICLE_AUDIO_FOCUS_INDEX_EXTERNAL_FOCUS_STATE);
- listener.onFocusChange(focusState, streams, externalFocus);
+ if (focusListener != null) {
+ focusListener.onFocusChange(focusState, streams, externalFocus);
+ }
+ } break;
+ case VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE: {
+ int state = v.getInt32Values(
+ VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STATE);
+ int streamNum = v.getInt32Values(
+ VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STREAM);
+ if (focusListener != null) {
+ focusListener.onStreamStatusChange(streamNum, state);
+ }
} break;
case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME: {
int volume = v.getInt32Values(
@@ -374,20 +417,22 @@
VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STREAM);
int volumeState = v.getInt32Values(
VehicleAudioVolumeIndex.VEHICLE_AUDIO_VOLUME_INDEX_STATE);
- listener.onVolumeChange(streamNum, volume, volumeState);
+ if (volumeListener != null) {
+ volumeListener.onVolumeChange(streamNum, volume, volumeState);
+ }
} break;
case VehicleNetworkConsts.VEHICLE_PROPERTY_AUDIO_VOLUME_LIMIT: {
- //TODO
- } break;
- case VehicleNetworkConsts.VEHICLE_PROPERTY_INTERNAL_AUDIO_STREAM_STATE: {
- int state = v.getInt32Values(
- VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STATE);
- int streamNum = v.getInt32Values(
- VehicleAudioStreamStateIndex.VEHICLE_AUDIO_STREAM_STATE_INDEX_STREAM);
- listener.onStreamStatusChange(streamNum, state);
+ int stream = v.getInt32Values(
+ VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_STREAM);
+ int maxVolume = v.getInt32Values(
+ VehicleAudioVolumeLimitIndex.VEHICLE_AUDIO_VOLUME_LIMIT_INDEX_MAX_VOLUME);
+ if (volumeListener != null) {
+ volumeListener.onVolumeLimitChange(stream, maxVolume);
+ }
} break;
}
}
+ values.clear();
}
@Override
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
index f347ce9..e730b03 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/input_test.xml
@@ -24,24 +24,11 @@
android:orientation="vertical"
android:layout_weight="1" />
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layout_weight="1" >
- <Button
- android:id="@+id/button_volume_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/volume_up" />
- <Button
- android:id="@+id/button_volume_down"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/volume_down" />
- <Button
- android:id="@+id/button_voice"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/voice" />
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:id="@+id/input_buttons">
+ <!-- Filled at runtime. -->
</LinearLayout>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
index c617094..7eac97c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/keyboard_test.xml
@@ -20,6 +20,12 @@
android:layout_marginLeft="96dp">
<TextView
+ android:id="@+id/driving_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="20dp"/>
+
+ <TextView
android:id="@+id/search_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml b/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml
new file mode 100644
index 0000000..cef8754
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/radio.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <!-- dummy one for top area -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:orientation="vertical"
+ android:layout_weight="1" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_weight="1" >
+ <Button
+ android:id="@+id/button_open_radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_open" />
+ <Button
+ android:id="@+id/button_close_radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_close" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_weight="1" >
+ <Button
+ android:id="@+id/button_get_focus_in_radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_get_focus" />
+ <Button
+ android:id="@+id/button_release_focus_in_radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_release_focus" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_weight="1" >
+ <ToggleButton
+ android:id="@+id/button_band_selection"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:textOn="@string/radio_fm"
+ android:textOff="@string/radio_am" />
+ <Button
+ android:id="@+id/button_radio_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_next" />
+ <Button
+ android:id="@+id/button_radio_prev"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_prev" />
+ <Button
+ android:id="@+id/button_radio_scan_cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/radio_scan_cancel" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_weight="1" >
+ <TextView
+ android:id="@+id/radio_station_info"
+ android:layout_marginRight="@dimen/radioInfoMargin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/radio_channel_info"
+ android:layout_marginRight="@dimen/radioInfoMargin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/radio_song_info"
+ android:layout_marginRight="@dimen/radioInfoMargin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/radio_artist_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_weight="1" >
+ <TextView
+ android:id="@+id/radio_log"
+ android:maxLines="@integer/radio_log_lines"
+ android:scrollbars="vertical"
+ android:gravity="bottom"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
index e35ee3d..17b9dbe 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/dimens.xml
@@ -18,4 +18,5 @@
<dimen name="rvcBtnWidth">150dp</dimen>
<dimen name="rvcTextSize">10dp</dimen>
<dimen name="rvcTvHeight">80dp</dimen>
+ <dimen name="radioInfoMargin">5dp</dimen>
</resources>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/integers.xml b/tests/EmbeddedKitchenSinkApp/res/values/integers.xml
new file mode 100644
index 0000000..e8b418f
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/values/integers.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<resources>
+ <integer name="radio_log_lines">5</integer>
+</resources>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 9eeeaa9..0861351 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -105,5 +105,35 @@
<!-- input test -->
<string name="volume_up">Volume +</string>
<string name="volume_down">Volume -</string>
+ <string name="volume_mute">Mute</string>
<string name="voice">Voice</string>
+ <string name="mock_vehicle_hal">Mock HAL</string>
+ <string name="mock_vehicle_hal_off">Mock HAL OFF</string>
+ <string name="mock_vehicle_hal_on">Mock HAL ON</string>
+ <string name="music">Music</string>
+ <string name="call_send">Call</string>
+ <string name="call_end">Call end</string>
+ <string name="home">Home</string>
+ <string name="next_song">Next song</string>
+ <string name="prev_song">Prev song</string>
+ <string name="tune_right">Tune +</string>
+ <string name="tune_left">Tune -</string>
+ <string name="music_play">Play</string>
+ <string name="music_stop">Stop</string>
+
+ <!-- radio test -->
+ <string name="radio_open">Open</string>
+ <string name="radio_close">Close</string>
+ <string name="radio_get_focus">Get Audio focus</string>
+ <string name="radio_release_focus">Release Audio focus</string>
+ <string name="radio_next">Next</string>
+ <string name="radio_prev">Previous</string>
+ <string name="radio_scan_cancel">Cancel scan</string>
+ <string name="radio_am">AM</string>
+ <string name="radio_fm">FM</string>
+ <string name="radio_station_info">Station info: %1$s</string>
+ <string name="radio_channel_info">Channel info: %1$s kHz</string>
+ <string name="radio_song_info">Song info: %1$s</string>
+ <string name="radio_artist_info">Artist info: %1$s</string>
+ <string name="radio_na">N/A</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index d227486..88270da 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -44,6 +44,7 @@
import com.google.android.car.kitchensink.input.InputTestFragment;
import com.google.android.car.kitchensink.job.JobSchedulerFragment;
import com.google.android.car.kitchensink.keyboard.KeyboardFragment;
+import com.google.android.car.kitchensink.radio.RadioTestFragment;
import java.util.ArrayList;
import java.util.List;
@@ -59,6 +60,7 @@
private static final String MENU_KEYBOARD = "keyboard";
private static final String MENU_CLUSTER = "inst cluster";
private static final String MENU_INPUT_TEST = "input test";
+ private static final String MENU_RADIO = "radio";
private Car mCarApi;
private CarCameraManager mCameraManager;
@@ -69,6 +71,7 @@
private AudioTestFragment mAudioTestFragment;
+ private RadioTestFragment mRadioTestFragment;
private CameraTestFragment mCameraTestFragment;
private HvacTestFragment mHvacTestFragment;
private JobSchedulerFragment mJobFragment;
@@ -213,8 +216,8 @@
List<CarMenu.Item> items = new ArrayList<>();
if (parentId.equals(ROOT)) {
String[] allMenus = {
- MENU_AUDIO, MENU_CAMERA, MENU_HVAC, MENU_JOB, MENU_KEYBOARD, MENU_CLUSTER,
- MENU_INPUT_TEST, MENU_QUIT
+ MENU_AUDIO, MENU_RADIO, MENU_CAMERA, MENU_HVAC, MENU_JOB, MENU_KEYBOARD,
+ MENU_CLUSTER, MENU_INPUT_TEST, MENU_QUIT
};
for (String menu : allMenus) {
items.add(new CarMenu.Builder(menu).setText(menu).build());
@@ -231,6 +234,11 @@
mAudioTestFragment = new AudioTestFragment();
}
setContentFragment(mAudioTestFragment);
+ } else if (id.equals(MENU_RADIO)) {
+ if (mRadioTestFragment == null) {
+ mRadioTestFragment = new RadioTestFragment();
+ }
+ setContentFragment(mRadioTestFragment);
} else if (id.equals(MENU_CAMERA)) {
if (mCameraManager != null) {
if (mCameraTestFragment == null) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
index 64ede01..a177c00 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
@@ -16,6 +16,7 @@
package com.google.android.car.kitchensink.input;
import android.annotation.Nullable;
+import android.annotation.StringRes;
import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.test.CarTestManager;
@@ -34,6 +35,17 @@
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.google.android.car.kitchensink.R;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHwKeyInputAction;
+import com.android.car.vehiclenetwork.VehiclePropValueUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import com.android.car.vehiclenetwork.VehicleNetworkConsts;
import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHwKeyInputAction;
@@ -49,11 +61,11 @@
private static final String TAG = "CAR.INPUT.KS";
+ private static final Button BREAK_LINE = null;
+
private Car mCar;
private CarTestManager mTestManager;
- private Button mVolumeUp;
- private Button mVolumeDown;
- private Button mVoice;
+ private final List<View> mButtons = new ArrayList<>();
@Nullable
@Override
@@ -61,32 +73,27 @@
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.input_test, container, false);
- // Single touch + key event does not work as touch is happening in other window
- // at the same time. But long press will work.
- mVolumeUp = (Button) view.findViewById(R.id.button_volume_up);
- mVolumeUp.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- handleTouchEvent(event, KeyEvent.KEYCODE_VOLUME_UP);
- return true;
- }
- });
- mVolumeDown = (Button) view.findViewById(R.id.button_volume_down);
- mVolumeDown.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- handleTouchEvent(event, KeyEvent.KEYCODE_VOLUME_DOWN);
- return true;
- }
- });
- mVoice = (Button) view.findViewById(R.id.button_voice);
- mVoice.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- handleTouchEvent(event, KeyEvent.KEYCODE_VOICE_ASSIST);
- return true;
- }
- });
+ 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)
+ );
+
+ addButtonsToPanel((LinearLayout) view.findViewById(R.id.input_buttons), mButtons);
mCar = Car.createCar(getContext(), new ServiceConnection() {
@Override
@@ -97,12 +104,16 @@
} catch (CarNotConnectedException e) {
throw new RuntimeException("Failed to create test service manager", e);
}
- if (!mTestManager.isPropertySupported(
- VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT)) {
+ boolean hwKeySupported = mTestManager.isPropertySupported(
+ VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT);
+ if (!hwKeySupported) {
Log.w(TAG, "VEHICLE_PROPERTY_HW_KEY_INPUT not supported");
- mVolumeUp.setEnabled(false);
- mVolumeDown.setEnabled(false);
- mVoice.setEnabled(false);
+ }
+
+ for (View v : mButtons) {
+ if (v != null) {
+ v.setEnabled(hwKeySupported);
+ }
}
}
@@ -114,6 +125,34 @@
return view;
}
+ private Button createButton(@StringRes int textResId, int keyCode) {
+ Button button = new Button(getContext());
+ button.setText(getContext().getString(textResId));
+ button.setTextSize(32f);
+ // Single touch + key event does not work as touch is happening in other window
+ // at the same time. But long press will work.
+ button.setOnTouchListener((v, event) -> {
+ handleTouchEvent(event, keyCode);
+ return true;
+ });
+
+ return button;
+ }
+
+ private void checkHwKeyInputSupported() {
+ boolean hwKeyInputSupported = mTestManager.isPropertySupported(
+ VehicleNetworkConsts.VEHICLE_PROPERTY_HW_KEY_INPUT);
+ if (!hwKeyInputSupported) {
+ Log.w(TAG, "VEHICLE_PROPERTY_HW_KEY_INPUT not supported");
+ }
+
+ for (View v : mButtons) {
+ if (v != null) {
+ v.setEnabled(hwKeyInputSupported);
+ }
+ }
+ }
+
private void handleTouchEvent(MotionEvent event, int keyCode) {
int action = event.getActionMasked();
Log.i(TAG, "handleTouchEvent, action:" + action + ",keyCode:" + keyCode);
@@ -139,4 +178,17 @@
super.onDestroyView();
mCar.disconnect();
}
+
+ private void addButtonsToPanel(LinearLayout root, List<View> buttons) {
+ LinearLayout panel = null;
+ for (View button : buttons) {
+ if (button == BREAK_LINE) {
+ panel = new LinearLayout(getContext());
+ panel.setOrientation(LinearLayout.HORIZONTAL);
+ root.addView(panel);
+ } else {
+ panel.addView(button);
+ }
+ }
+ }
}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
index 4ee0b16..42fc6d7 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/keyboard/KeyboardFragment.java
@@ -18,9 +18,15 @@
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
+import android.support.car.Car;
+import android.support.car.CarNotConnectedException;
+import android.support.car.CarNotSupportedException;
import android.support.car.app.menu.CarDrawerActivity;
import android.support.car.app.menu.SearchBoxEditListener;
+import android.support.car.hardware.CarSensorEvent;
+import android.support.car.hardware.CarSensorManager;
import android.support.v4.app.Fragment;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,16 +36,19 @@
import com.google.android.car.kitchensink.R;
public class KeyboardFragment extends Fragment {
+ private static final String TAG = "KitchenSinkKeyboard";
public static final int CARD = 0xfffafafa;
public static final int TEXT_PRIMARY_DAY = 0xde000000;
public static final int TEXT_SECONDARY_DAY = 0x8a000000;
+ private TextView mDrivingStatus;
private Button mImeButton;
private Button mCloseImeButton;
private Button mShowHideInputButton;
private CarDrawerActivity mActivity;
private TextView mOnSearchText;
private TextView mOnEditText;
+ private CarSensorManager mSensorManager;
private final Handler mHandler = new Handler();
@@ -81,10 +90,53 @@
mOnEditText = (TextView) v.findViewById(R.id.edit_text);
resetInput();
mActivity.setSearchBoxEndView(View.inflate(getContext(), R.layout.keyboard_end_view, null));
-
+ mDrivingStatus = (TextView) v.findViewById(R.id.driving_status);
return v;
}
+ @Override
+ public void onResume() {
+ super.onResume();
+ try {
+ mSensorManager = (CarSensorManager)
+ mActivity.getCar().getCarManager(Car.SENSOR_SERVICE);
+ mSensorManager.registerListener(mCarSensorListener,
+ CarSensorManager.SENSOR_TYPE_DRIVING_STATUS,
+ CarSensorManager.SENSOR_RATE_FASTEST);
+ } catch (CarNotSupportedException | CarNotConnectedException e) {
+ Log.e(TAG, "Car not connected or not supported", e);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mSensorManager != null) {
+ mSensorManager.unregisterListener(mCarSensorListener);
+ }
+ }
+
+ private final CarSensorManager.CarSensorEventListener mCarSensorListener =
+ new CarSensorManager.CarSensorEventListener() {
+ @Override
+ public void onSensorChanged(CarSensorEvent event) {
+ if (event.sensorType != CarSensorManager.SENSOR_TYPE_DRIVING_STATUS) {
+ return;
+ }
+ int drivingStatus = event.getDrivingStatusData(null).status;
+
+ boolean keyboardEnabled =
+ (drivingStatus & CarSensorEvent.DRIVE_STATUS_NO_KEYBOARD_INPUT) == 0;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mDrivingStatus.setText("Driving status: " + drivingStatus
+ + " Keyboard " + (keyboardEnabled ? "enabled" : "disabled"));
+ }
+ });
+ }
+ };
+
private void resetInput() {
mActivity.showSearchBox(new View.OnClickListener() {
@Override
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java
new file mode 100644
index 0000000..c812b7b
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/radio/RadioTestFragment.java
@@ -0,0 +1,420 @@
+/*
+ * 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.radio;
+
+import android.annotation.Nullable;
+import android.car.Car;
+import android.car.media.CarAudioManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import com.google.android.car.kitchensink.R;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class RadioTestFragment extends Fragment {
+ private static final String TAG = "CAR.RADIO.KS";
+ private static final boolean DBG = true;
+ private static final int MAX_LOG_MESSAGES = 100;
+
+ private final AudioManager.OnAudioFocusChangeListener mRadioFocusListener =
+ new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ addLog(Log.INFO, "Radio focus change:" + focusChange);
+ }
+ };
+
+ private final AudioManager.OnAudioFocusChangeListener mSecondaryFocusListener =
+ new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ addLog(Log.INFO, "Secondary focus change:" + focusChange);
+ }
+ };
+
+ private final RadioTuner.Callback mRadioCallback = new RadioTuner.Callback() {
+ @Override
+ public void onError(int status) {
+ addLog(Log.WARN, "Radio tuner error " + status);
+ }
+
+ @Override
+ public void onConfigurationChanged(RadioManager.BandConfig config) {
+ addLog(Log.INFO, "Radio tuner configuration changed. config:" + config);
+ }
+
+ @Override
+ public void onMetadataChanged(RadioMetadata metadata) {
+ addLog(Log.INFO, "Radio tuner metadata changed. metadata:" + metadata);
+ if (metadata == null) {
+ resetMessages();
+ updateMessages();
+ return;
+ }
+ mArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST);
+ mSong = metadata.getString(RadioMetadata.METADATA_KEY_TITLE);
+ mStation = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PI);
+ updateMessages();
+ }
+
+ @Override
+ public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
+ addLog(Log.INFO, "Radio tuner program info. info:" + info);
+ mChannel = String.valueOf(info.getChannel());
+ onMetadataChanged(info.getMetadata());
+ updateMessages();
+ }
+
+ };
+ private final LinkedList<String> mLogMessages = new LinkedList<>();
+
+ private Button mOpenRadio;
+ private Button mCloseRadio;
+ private Button mGetFocus;
+ private Button mReleaseFocus;
+ private Button mRadioNext;
+ private Button mRadioPrev;
+ private Button mRadioScanCancel;
+ private ToggleButton mRadioBand;
+ private TextView mStationInfo;
+ private TextView mChannelInfo;
+ private TextView mSongInfo;
+ private TextView mArtistInfo;
+ private TextView mLog;
+
+ private Car mCar;
+ private CarAudioManager mCarAudioManager;
+ private AudioAttributes mRadioAudioAttrib;
+ private AudioManager mAudioManager;
+ private boolean hasSecondaryFocus;
+ private boolean isScanning;
+ private RadioTuner mRadioTuner;
+ private RadioManager mRadioManager;
+ private RadioManager.FmBandDescriptor mFmDescriptor;
+ private RadioManager.AmBandDescriptor mAmDescriptor;
+ private String mStation;
+ private String mChannel;
+ private String mSong;
+ private String mArtist;
+ private String mNaString;
+
+ private RadioManager.BandConfig mFmConfig;
+ private RadioManager.BandConfig mAmConfig;
+
+ private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ if (DBG) {
+ Log.i(TAG, "onCreateView");
+ }
+
+ init();
+ View view = inflater.inflate(R.layout.radio, container, false);
+
+ mOpenRadio = (Button) view.findViewById(R.id.button_open_radio);
+ mCloseRadio = (Button) view.findViewById(R.id.button_close_radio);
+ mGetFocus = (Button) view.findViewById(R.id.button_get_focus_in_radio);
+ mReleaseFocus = (Button) view.findViewById(R.id.button_release_focus_in_radio);
+ mRadioNext = (Button) view.findViewById(R.id.button_radio_next);
+ mRadioPrev = (Button) view.findViewById(R.id.button_radio_prev);
+ mRadioScanCancel = (Button) view.findViewById(R.id.button_radio_scan_cancel);
+ mRadioBand = (ToggleButton) view.findViewById(R.id.button_band_selection);
+
+ mStationInfo = (TextView) view.findViewById(R.id.radio_station_info);
+ mChannelInfo = (TextView) view.findViewById(R.id.radio_channel_info);
+ mSongInfo = (TextView) view.findViewById(R.id.radio_song_info);
+ mArtistInfo = (TextView) view.findViewById(R.id.radio_artist_info);
+
+ mLog = (TextView) view.findViewById(R.id.radio_log);
+ mLog.setMovementMethod(new ScrollingMovementMethod());
+
+ mNaString = getContext().getString(R.string.radio_na);
+
+ addHandlers();
+ updateStates();
+
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ resetMessages();
+ updateStates();
+ updateMessages();
+ resetLog();
+ }
+
+ private void init() {
+ mCar = Car.createCar(getContext(), new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+ mRadioAudioAttrib = mCarAudioManager.getAudioAttributesForCarUsage(
+ CarAudioManager.CAR_AUDIO_USAGE_RADIO);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ }
+ }, Looper.getMainLooper());
+ mCar.connect();
+ mAudioManager = (AudioManager) getContext().getSystemService(
+ Context.AUDIO_SERVICE);
+ initializeRadio();
+ }
+
+ private void initializeRadio() {
+ mRadioManager = (RadioManager) getContext().getSystemService(Context.RADIO_SERVICE);
+
+ if (mRadioManager == null) {
+ throw new IllegalStateException("RadioManager could not be loaded.");
+ }
+
+ int status = mRadioManager.listModules(mModules);
+ if (status != RadioManager.STATUS_OK) {
+ throw new IllegalStateException("Load modules failed with status: " + status);
+ }
+
+ if (mModules.size() == 0) {
+ throw new IllegalStateException("No radio modules on device.");
+ }
+
+ boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Load the possible radio bands. For now, just accept FM and AM bands.
+ for (RadioManager.BandDescriptor band : mModules.get(0).getBands()) {
+ if (isDebugLoggable) {
+ Log.d(TAG, "loading band: " + band.toString());
+ }
+
+ if (mFmDescriptor == null && band.getType() == RadioManager.BAND_FM) {
+ mFmDescriptor = (RadioManager.FmBandDescriptor) band;
+ }
+
+ if (mAmDescriptor == null && band.getType() == RadioManager.BAND_AM) {
+ mAmDescriptor = (RadioManager.AmBandDescriptor) band;
+ }
+ }
+
+ if (mFmDescriptor == null && mAmDescriptor == null) {
+ throw new IllegalStateException("No AM and FM radio bands could be loaded.");
+ }
+
+ mFmConfig = new RadioManager.FmBandConfig.Builder(mFmDescriptor)
+ .setStereo(true)
+ .build();
+ mAmConfig = new RadioManager.AmBandConfig.Builder(mAmDescriptor)
+ .setStereo(true)
+ .build();
+ }
+
+ private void addHandlers() {
+ mOpenRadio.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handleRadioStart();
+ updateStates();
+ }
+ });
+ mCloseRadio.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handleRadioEnd();
+ updateStates();
+ }
+ });
+ mGetFocus.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (DBG) {
+ Log.i(TAG, "Get secondary focus");
+ }
+ mAudioManager.requestAudioFocus(mSecondaryFocusListener,
+ AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ hasSecondaryFocus = true;
+ updateStates();
+ }
+ });
+ mReleaseFocus.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (DBG) {
+ Log.i(TAG, "Release secondary focus");
+ }
+ mAudioManager.abandonAudioFocus(mSecondaryFocusListener);
+ hasSecondaryFocus = false;
+ updateStates();
+ }
+ });
+ mRadioNext.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (DBG) {
+ Log.i(TAG, "Next radio station");
+ }
+ if (mRadioTuner != null) {
+ mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
+ }
+ updateStates();
+ }
+ });
+ mRadioPrev.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (DBG) {
+ Log.i(TAG, "Previous radio station");
+ }
+ if (mRadioTuner != null) {
+ mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, true);
+ }
+ updateStates();
+ }
+ });
+ mRadioScanCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (DBG) {
+ Log.i(TAG, "Cancel radio scan");
+ }
+ if (mRadioTuner != null) {
+ mRadioTuner.cancel();
+ }
+ updateStates();
+ }
+ });
+ mRadioBand.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (DBG) {
+ Log.i(TAG, "Changing ratio band");
+ }
+ if (mRadioTuner != null) {
+ mRadioTuner.close();
+ mRadioTuner = null;
+ mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(),
+ mRadioBand.isChecked() ? mFmConfig : mAmConfig, true, mRadioCallback , null);
+ }
+ resetMessages();
+ updateMessages();
+ updateStates();
+ }
+ });
+ }
+
+ private void updateStates() {
+ mOpenRadio.setEnabled(mRadioTuner == null);
+ mCloseRadio.setEnabled(mRadioTuner != null);
+ mGetFocus.setEnabled(!hasSecondaryFocus);
+ mReleaseFocus.setEnabled(hasSecondaryFocus);
+ mRadioNext.setEnabled(mRadioTuner != null);
+ mRadioPrev.setEnabled(mRadioTuner != null);
+ mRadioBand.setEnabled(mRadioTuner != null);
+ }
+
+ private void updateMessages() {
+ mStationInfo.setText(getContext().getString
+ (R.string.radio_station_info, mStation == null ? mNaString : mStation));
+ mChannelInfo.setText(getContext().getString
+ (R.string.radio_channel_info, mChannel == null ? mNaString : mChannel));
+ mArtistInfo.setText(getContext().getString
+ (R.string.radio_artist_info, mArtist == null ? mNaString : mArtist));
+ mSongInfo.setText(getContext().getString
+ (R.string.radio_song_info, mSong == null ? mNaString : mSong));
+ }
+
+ private void resetMessages() {
+ mStation = null;
+ mChannel = null;
+ mSong = null;
+ mArtist = null;
+ }
+
+ private void handleRadioStart() {
+ if (mCarAudioManager == null) {
+ return;
+ }
+ if (DBG) {
+ Log.i(TAG, "Radio start");
+ }
+ mCarAudioManager.requestAudioFocus(mRadioFocusListener, mRadioAudioAttrib,
+ AudioManager.AUDIOFOCUS_GAIN, 0);
+ if (mRadioTuner != null) {
+ mRadioTuner.close();
+ mRadioTuner = null;
+ }
+ mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), mRadioBand.isChecked() ? mFmConfig : mAmConfig,
+ true, mRadioCallback /* callback */, null /* handler */);
+ }
+
+ private void handleRadioEnd() {
+ if (mCarAudioManager == null) {
+ return;
+ }
+ if (DBG) {
+ Log.i(TAG, "Radio end");
+ }
+ mCarAudioManager.abandonAudioFocus(mRadioFocusListener, mRadioAudioAttrib);
+ mRadioTuner.close();
+ mRadioTuner = null;
+ }
+
+ private void resetLog() {
+ synchronized (this) {
+ mLogMessages.clear();
+ }
+ }
+
+ private void addLog(int priority, String message) {
+ Log.println(priority, TAG, message);
+ synchronized (this) {
+ mLogMessages.add(message);
+ if (mLogMessages.size() > MAX_LOG_MESSAGES) {
+ mLogMessages.poll();
+ }
+ mLog.setText(TextUtils.join("\n", mLogMessages));
+ }
+ }
+}
+
diff --git a/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java b/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
index 72e97e0..371a9a5 100644
--- a/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
+++ b/tests/carservice_test/src/com/android/car/test/AudioRoutingPolicyTest.java
@@ -117,6 +117,7 @@
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_ALARM_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG |
+ VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NOTIFICATION_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG |
@@ -138,6 +139,7 @@
assertEquals(
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_CALL_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_MUSIC_FLAG |
+ VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG |
VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_UNKNOWN_FLAG,
v.getInt32Values(
VehicleAudioRoutingPolicyIndex.VEHICLE_AUDIO_ROUTING_POLICY_INDEX_CONTEXTS)
diff --git a/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java b/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
index 180b3ef..29a020e 100644
--- a/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/test/CarAudioFocusTest.java
@@ -361,8 +361,7 @@
assertEquals(0, request[1]);
assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
request[2]);
- // no android side context for radio
- assertEquals(0, request[3]);
+ assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
mAudioFocusPropertyHandler.sendAudioFocusState(
VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
0,
@@ -382,7 +381,8 @@
assertEquals(0x1 << VehicleAudioStream.VEHICLE_AUDIO_STREAM1, request[1]);
assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
request[2]);
- assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG, request[3]);
+ assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_NAVIGATION_FLAG |
+ VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
mAudioFocusPropertyHandler.sendAudioFocusState(
VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
0x1 << VehicleAudioStream.VEHICLE_AUDIO_STREAM1,
@@ -396,7 +396,7 @@
assertEquals(0, request[1]);
assertEquals(VehicleAudioExtFocusFlag.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG,
request[2]);
- assertEquals(0, request[3]);
+ assertEquals(VehicleAudioContextFlag.VEHICLE_AUDIO_CONTEXT_RADIO_FLAG, request[3]);
mAudioFocusPropertyHandler.sendAudioFocusState(
VehicleAudioFocusState.VEHICLE_AUDIO_FOCUS_STATE_GAIN,
0,
diff --git a/tests/vehicle_hal_test/Android.mk b/tests/vehicle_hal_test/Android.mk
new file mode 100644
index 0000000..77bc6f8
--- /dev/null
+++ b/tests/vehicle_hal_test/Android.mk
@@ -0,0 +1,39 @@
+# 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.
+#
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := AndroidVehicleHalTests
+
+# for system|priviledged permission.
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := tests
+
+# When built explicitly put it in the data partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_JAVA_LIBRARIES := libvehiclenetwork-java
+
+LOCAL_JAVA_LIBRARIES := android.car android.test.runner
+
+include $(BUILD_PACKAGE)
diff --git a/tests/vehicle_hal_test/AndroidManifest.xml b/tests/vehicle_hal_test/AndroidManifest.xml
new file mode 100644
index 0000000..5fbb62d
--- /dev/null
+++ b/tests/vehicle_hal_test/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="com.android.car.vehiclenetwork.haltest"
+ android:sharedUserId="android.uid.system" >
+
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.car.vehiclenetwork.haltest"
+ android:label="Tests for vehicle hal using vehicle network service"/>
+
+ <application android:label="VehicleHalTest">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name=".TestCarProxyActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java
new file mode 100644
index 0000000..6111746
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/HvacVnsHalTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.android.car.vehiclenetwork.haltest;
+
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_AC_ON;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_DEFROSTER;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_FAN_DIRECTION;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_FAN_SPEED;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_MAX_AC_ON;
+import static com.android.car.vehiclenetwork.VehicleNetworkConsts.VEHICLE_PROPERTY_HVAC_RECIRC_ON;
+
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleHvacFanDirection;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehiclePropAccess;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
+
+/**
+ * Test HVAC vehicle HAL using vehicle network service.
+ */
+public class HvacVnsHalTest extends VnsHalBaseTestCase {
+
+ public void testAcProperty() throws Exception {
+ int propertyId = VEHICLE_PROPERTY_HVAC_AC_ON;
+ if (!isPropertyAvailable(propertyId)) {
+ return;
+ }
+
+ VehiclePropConfig config = mConfigsMap.get(propertyId);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ int zone = getFirstZoneForProperty(propertyId);
+ setPropertyAndVerify(propertyId, zone, false);
+
+ mVehicleNetwork.subscribe(propertyId, 0, 0);
+ mListener.reset();
+ mListener.addExpectedValues(propertyId, zone, true);
+ setPropertyAndVerify(propertyId, zone, true);
+ mListener.waitAndVerifyValues();
+ }
+
+ public void testRecirculateProperty() {
+ int propertyId = VEHICLE_PROPERTY_HVAC_RECIRC_ON;
+ if (!isPropertyAvailable(propertyId)) {
+ return;
+ }
+ VehiclePropConfig config = mConfigsMap.get(propertyId);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ // Verify subsequent calls ends-up with right value.
+ mVehicleNetwork.setBooleanProperty(propertyId, false);
+ mVehicleNetwork.setBooleanProperty(propertyId, true);
+ mVehicleNetwork.setBooleanProperty(propertyId, false);
+ mVehicleNetwork.setBooleanProperty(propertyId, true);
+ verifyValue(propertyId, true);
+
+ // Verify subsequent calls ends-up with right value.
+ mVehicleNetwork.setBooleanProperty(propertyId, false);
+ mVehicleNetwork.setBooleanProperty(propertyId, true);
+ mVehicleNetwork.setBooleanProperty(propertyId, false);
+ verifyValue(propertyId, false);
+
+ setPropertyAndVerify(propertyId, false);
+ setPropertyAndVerify(propertyId, true);
+ setPropertyAndVerify(propertyId, false);
+ }
+
+ public void testMaxAcProperty() throws Exception {
+ if (!isPropertyAvailable(
+ VEHICLE_PROPERTY_HVAC_MAX_AC_ON,
+ VEHICLE_PROPERTY_HVAC_AC_ON,
+ VEHICLE_PROPERTY_HVAC_RECIRC_ON)) {
+ return;
+ }
+ VehiclePropConfig config = mConfigsMap.get(VEHICLE_PROPERTY_HVAC_MAX_AC_ON);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ int acZone = getFirstZoneForProperty(VEHICLE_PROPERTY_HVAC_AC_ON);
+
+ // Turn off related properties.
+ setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, false);
+ setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, false);
+ setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_RECIRC_ON, false);
+
+ // Now turning max A/C and verify that other related HVAC props turned on.
+ mVehicleNetwork.subscribe(VEHICLE_PROPERTY_HVAC_AC_ON, 0f, acZone);
+ mVehicleNetwork.subscribe(VEHICLE_PROPERTY_HVAC_RECIRC_ON, 0f);
+ mListener.reset();
+ mListener.addExpectedValues(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+ mListener.addExpectedValues(VEHICLE_PROPERTY_HVAC_RECIRC_ON, 0, true);
+ setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, true);
+ verifyValue(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+ verifyValue(VEHICLE_PROPERTY_HVAC_RECIRC_ON, true);
+ mListener.waitAndVerifyValues();
+
+ // When max A/C turned off, A/C should remain to be turned on, but circulation should has
+ // the value it had before turning max A/C on, which if OFF in this case.
+ setPropertyAndVerify(VEHICLE_PROPERTY_HVAC_MAX_AC_ON, false);
+ verifyValue(VEHICLE_PROPERTY_HVAC_AC_ON, acZone, true);
+ verifyValue(VEHICLE_PROPERTY_HVAC_RECIRC_ON, false);
+ }
+
+ public void testDefroster() {
+ final int propertyId = VEHICLE_PROPERTY_HVAC_DEFROSTER;
+ if (!isPropertyAvailable(propertyId)) {
+ return;
+ }
+
+ VehiclePropConfig config = mConfigsMap.get(propertyId);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+ Log.i(TAG, "testDefroster, " + message);
+ setPropertyAndVerify(propertyId, zone, false);
+ setPropertyAndVerify(propertyId, zone, true);
+ });
+ }
+
+ public void testFanSpeed() throws Exception {
+ if (!isPropertyAvailable(VEHICLE_PROPERTY_HVAC_FAN_SPEED)) {
+ return;
+ }
+
+ VehiclePropConfig config = mConfigsMap.get(VEHICLE_PROPERTY_HVAC_FAN_SPEED);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ verifyIntZonedProperty(VEHICLE_PROPERTY_HVAC_FAN_SPEED);
+ }
+
+ public void testFanDirection() throws Exception {
+ final int propertyId = VEHICLE_PROPERTY_HVAC_FAN_DIRECTION;
+
+ if (!isPropertyAvailable(propertyId)) {
+ return;
+ }
+
+ VehiclePropConfig config = mConfigsMap.get(propertyId);
+ assertEquals(VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32, config.getValueType());
+ assertEquals(VehiclePropAccess.VEHICLE_PROP_ACCESS_READ_WRITE, config.getAccess());
+
+ // Assert setting edge-case values.
+ iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+ setPropertyAndVerify(message, propertyId, zone, minValue);
+ setPropertyAndVerify(message, propertyId, zone, maxValue);
+ });
+
+ iterateOnZones(config, ((message, zone, minValue, maxValue)
+ -> setPropertyAndVerify(message, propertyId, zone,
+ VehicleHvacFanDirection.VEHICLE_HVAC_FAN_DIRECTION_DEFROST_AND_FLOOR)));
+ }
+}
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java
new file mode 100644
index 0000000..c10dbd0
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/RecordingVehicleNetworkListener.java
@@ -0,0 +1,210 @@
+/*
+ * 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.android.car.vehiclenetwork.haltest;
+
+import static java.lang.Integer.toHexString;
+import static junit.framework.Assert.assertTrue;
+
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetwork.VehicleNetworkListener;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValue;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropValues;
+import com.android.car.vehiclenetwork.VehicleNetworkProtoUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class must be used in testing environment only. Here's an example of usage:
+ * <ul>
+ * <li>listener.reset();
+ * <li>listener.addExpectedValues(myPropertyId, myZone, 10, 20, 30)
+ * <li>... set values through VehicleNetworkService ...
+ * <li>listener.waitAndVerifyValues()
+ *</ul>
+ *
+ */
+class RecordingVehicleNetworkListener implements VehicleNetworkListener {
+
+ private final static String TAG = VnsHalBaseTestCase.TAG;
+ private final static int EVENTS_WAIT_TIMEOUT_MS = 2000;
+
+ private final Set<NormalizedValue> mExpectedValues = new HashSet<>();
+ // Using Set here instead of List as we probably shouldn't assert the order event was received.
+ private final Set<NormalizedValue> mRecordedEvents = new HashSet<>();
+
+ synchronized void reset() {
+ mExpectedValues.clear();
+ mRecordedEvents.clear();
+ }
+
+ void addExpectedValues(int propertyId, int zone, Object... values) {
+ for (Object value : values) {
+ mExpectedValues.add(NormalizedValue.createFor(propertyId, zone, value));
+ }
+ }
+
+ /**
+ * Waits for events to come for #EVENTS_WAIT_TIMEOUT_MS milliseconds and asserts that recorded
+ * values match with expected.
+ * */
+ synchronized void waitAndVerifyValues() throws InterruptedException {
+ long currentTime = System.currentTimeMillis();
+ long deadline = currentTime + EVENTS_WAIT_TIMEOUT_MS;
+ while (currentTime < deadline && !isExpectedMatchedRecorded()) {
+ wait(deadline - currentTime);
+ currentTime = System.currentTimeMillis();
+ }
+ assertTrue("Expected values: " + Arrays.toString(mExpectedValues.toArray())
+ + " doesn't match recorded: " + Arrays.toString(mRecordedEvents.toArray()),
+ isExpectedMatchedRecorded());
+ }
+
+ private boolean isExpectedMatchedRecorded() {
+ for (NormalizedValue expectedValue : mExpectedValues) {
+ if (!mRecordedEvents.contains(expectedValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onVehicleNetworkEvents(VehiclePropValues values) {
+ for (VehiclePropValue value : values.getValuesList()) {
+ Log.d(TAG, "onVehicleNetworkEvents, value: "
+ + VehicleNetworkProtoUtil.VehiclePropValueToString(value));
+
+ synchronized (this) {
+ mRecordedEvents.add(NormalizedValue.createFor(value));
+ notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public void onHalError(int errorCode, int property, int operation) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void onHalRestart(boolean inMocking) {
+ // TODO Auto-generated method stub
+ }
+
+ // To be used to compare expected vs recorded values.
+ private static class NormalizedValue {
+ private final int propertyId;
+ private final int zone;
+ private final List<Object> value;
+
+ static NormalizedValue createFor(VehiclePropValue value) {
+ return new NormalizedValue(value.getProp(), value.getZone(), getObjectValue(value));
+ }
+
+ static NormalizedValue createFor(int propertyId, int zone, Object value) {
+ return new NormalizedValue(propertyId, zone, wrapSingleObjectToList(value));
+ }
+
+ // Do not call this ctor directly, use appropriate factory methods to create an object.
+ private NormalizedValue(int propertyId, int zone, List<Object> value) {
+ this.propertyId = propertyId;
+ this.zone = zone;
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NormalizedValue propValue = (NormalizedValue) o;
+ return propertyId == propValue.propertyId &&
+ zone == propValue.zone &&
+ Objects.equals(value, propValue.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(propertyId, zone, value);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " { "
+ + "prop: 0x" + toHexString(propertyId)
+ + ", zone: 0x" + toHexString(zone)
+ + ", value: " + Arrays.toString(value.toArray())
+ + " }";
+ }
+
+ private static List<Object> wrapSingleObjectToList(Object value) {
+ if (value instanceof Integer
+ || value instanceof Float) {
+ List<Object> list = new ArrayList<>(1);
+ list.add(value);
+ return list;
+ } else if (value instanceof Boolean) {
+ List<Object> list = new ArrayList<>(1);
+ list.add((Boolean)value ? 1 : 0);
+ return list;
+ } else if (value instanceof Collection<?>) {
+ return new ArrayList<>((Collection<?>) value);
+ } else {
+ throw new IllegalArgumentException("Unexpected type: " + value);
+ }
+ }
+
+ // Converts any VehiclePropValue to either ArrayList<Integer> or ArrayList<Float>
+ private static List<Object> getObjectValue(VehiclePropValue val) {
+ switch (val.getValueType()) {
+ case VehicleValueType.VEHICLE_VALUE_TYPE_BOOLEAN:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_INT32:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC2:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC3:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_INT32_VEC4:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC2:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC3:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_INT32_VEC4:
+ return new ArrayList<>(val.getInt32ValuesList());
+ case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC2:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC3:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_FLOAT_VEC4:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC2:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC3:
+ case VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_FLOAT_VEC4:
+ return new ArrayList<>(val.getFloatValuesList());
+ default:
+ throw new IllegalArgumentException("Unexpected type: " + val.getValueType());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java
new file mode 100644
index 0000000..adcc336
--- /dev/null
+++ b/tests/vehicle_hal_test/src/com/android/car/vehiclenetwork/haltest/VnsHalBaseTestCase.java
@@ -0,0 +1,212 @@
+/*
+ * 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.android.car.vehiclenetwork.haltest;
+
+import static java.lang.Integer.toHexString;
+
+import android.car.VehicleZoneUtil;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.car.vehiclenetwork.VehicleNetwork;
+import com.android.car.vehiclenetwork.VehicleNetworkConsts.VehicleValueType;
+import com.android.car.vehiclenetwork.VehicleNetworkProto.VehiclePropConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for testing vehicle HAL using vehicle network service (VNS).
+ */
+public class VnsHalBaseTestCase extends AndroidTestCase {
+
+ protected static final String TAG = "VnsHalTest";
+
+ protected static final int PROP_TIMEOUT_MS = 5000;
+ protected static final int NO_ZONE = -1;
+
+ private final HandlerThread mHandlerThread =
+ new HandlerThread(VnsHalBaseTestCase.class.getSimpleName());
+ protected VehicleNetwork mVehicleNetwork;
+ protected RecordingVehicleNetworkListener mListener;
+
+ protected Map<Integer, VehiclePropConfig> mConfigsMap;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHandlerThread.start();
+ mListener = new RecordingVehicleNetworkListener();
+ mVehicleNetwork = VehicleNetwork.createVehicleNetwork(mListener,
+ mHandlerThread.getLooper());
+ setupConfigsMap();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mHandlerThread.quit();
+ }
+
+ private void setupConfigsMap() {
+ mConfigsMap = new HashMap<>();
+ for (VehiclePropConfig config : mVehicleNetwork.listProperties().getConfigsList()) {
+ mConfigsMap.put(config.getProp(), config);
+ }
+ assertTrue(mConfigsMap.size() > 0);
+ }
+
+ protected boolean isPropertyAvailable(int... properties) {
+ assertTrue(properties.length > 0);
+
+ for (int propertyId : properties) {
+ if (!mConfigsMap.containsKey(propertyId)) {
+ // Property is not supported by vehicle HAL, nothing to test.
+ Log.w(TAG, "Property: 0x" + Integer.toHexString(propertyId)
+ + " is not available, ignoring...");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected void setPropertyAndVerify(int propertyId, boolean switchOn) {
+ setPropertyAndVerify(propertyId, NO_ZONE, switchOn);
+ }
+
+ protected void setPropertyAndVerify(int propertyId, int zone, boolean switchOn) {
+ int val = switchOn ? 1 : 0;
+
+ if (zone == NO_ZONE) {
+ mVehicleNetwork.setBooleanProperty(propertyId, switchOn);
+ } else {
+ mVehicleNetwork.setZonedBooleanProperty(propertyId, zone, switchOn);
+ }
+ verifyValue(propertyId, zone, val);
+ }
+
+ protected void setPropertyAndVerify(String message, int propertyId, int zone, int value) {
+ if (zone == NO_ZONE) {
+ mVehicleNetwork.setIntProperty(propertyId, value);
+ } else {
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, value);
+ }
+ verifyValue(message, propertyId, zone, value);
+ }
+
+ protected void verifyValue(int propertyId, boolean val) {
+ verifyValue(propertyId, NO_ZONE, val);
+ }
+
+ protected void verifyValue(int propertyId, int zone, boolean val) {
+ int intVal = val ? 1 : 0;
+ assertEquals(intVal, waitForIntValue(propertyId, zone, intVal));
+ }
+
+ protected void verifyValue(int propertyId, int zone, int val) {
+ assertEquals(val, waitForIntValue(propertyId, zone, val));
+ }
+
+ protected void verifyValue(String message, int propertyId, int zone, int val) {
+ assertEquals(message, val, waitForIntValue(propertyId, zone, val));
+ }
+
+ protected int getFirstZoneForProperty(int propertyId) {
+ return VehicleZoneUtil.getFirstZone(mConfigsMap.get(propertyId).getZones());
+ }
+
+ protected void verifyIntZonedProperty(int propertyId) throws InterruptedException {
+ if (!isPropertyAvailable(propertyId)) {
+ return;
+ }
+
+ VehiclePropConfig config = mConfigsMap.get(propertyId);
+
+ // Assert setting edge-case values.
+ iterateOnZones(config, (message, zone, minValue, maxValue) -> {
+ setPropertyAndVerify(message, propertyId, zone, minValue);
+ setPropertyAndVerify(message, propertyId, zone, maxValue);
+ });
+
+ // Setting out of the range values.
+ iterateOnZones(config, ((message, zone, minValue, maxValue) -> {
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, minValue - 1);
+ verifyValue(message, propertyId, zone, minValue);
+
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, maxValue + 20);
+ verifyValue(message, propertyId, zone, maxValue);
+ }));
+
+ // Verify that subsequent SET calls will result in correct value at the end.
+ mVehicleNetwork.subscribe(propertyId, 0f, config.getZones());
+ int zone = VehicleZoneUtil.getFirstZone(config.getZones());
+ int minValue = config.getInt32Mins(0);
+ int maxValue = config.getInt32Maxs(0);
+ int finalValue = (minValue + maxValue) / 2;
+ mListener.reset();
+ // We can only expect to see finalValue in the events as vehicle HAL may batch
+ // set commands and use only last value.
+ mListener.addExpectedValues(propertyId, zone, finalValue);
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, minValue);
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, maxValue);
+ mVehicleNetwork.setZonedIntProperty(propertyId, zone, finalValue);
+ verifyValue(propertyId, zone, finalValue);
+ mListener.waitAndVerifyValues();
+ mVehicleNetwork.unsubscribe(propertyId);
+ }
+
+ protected int waitForIntValue(int propertyId, int zone, int expectedValue) {
+ int actualValue = mVehicleNetwork.getZonedIntProperty(propertyId, zone);
+ long deadline = System.currentTimeMillis() + PROP_TIMEOUT_MS;
+
+ while (System.currentTimeMillis() <= deadline && actualValue != expectedValue) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ throw new RuntimeException(ex);
+ }
+ actualValue = mVehicleNetwork.getZonedIntProperty(propertyId, zone);
+ }
+
+ return actualValue;
+ }
+
+ protected void iterateOnZones(VehiclePropConfig config, IntZonesFunctor f) {
+ int[] zones = VehicleZoneUtil.listAllZones(config.getZones());
+ int zoneIndex = 0;
+ boolean isBooleanType = config.getValueType() ==
+ VehicleValueType.VEHICLE_VALUE_TYPE_ZONED_BOOLEAN;
+
+ for (int zone : zones) {
+ int minValue = isBooleanType ? 0 : config.getInt32Mins(zoneIndex);
+ int maxValue = isBooleanType ? 1 : config.getInt32Maxs(zoneIndex);
+ String message = "PropertyId: 0x" + toHexString(config.getProp())
+ + ", zone[" + zoneIndex + "]: 0x" + toHexString(zone);
+ f.func(message, zone, minValue, maxValue);
+ zoneIndex++;
+ }
+
+ }
+
+ protected interface IntZonesFunctor {
+ void func(String message, int zone, int minValue, int maxValue);
+ }
+
+
+}