From b0f1aa29067bc62fde627857a22414401710395b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 24 Jan 2024 16:57:27 +0100 Subject: [PATCH] Integrate car sensors with automotive, and added permissions (#4122) * Integrate car sensors with automotive, and added permissions * Set context in every call * Fixed CarSensorManager format * Enable car sensors only on correct flavors * Fixed format * Renamed context to latestContext and added parenthesis to expression --- .../android/sensors/CarSensorManager.kt | 283 +++++++++++------- automotive/src/main/AndroidManifest.xml | 4 + 2 files changed, 177 insertions(+), 110 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/sensors/CarSensorManager.kt b/app/src/main/java/io/homeassistant/companion/android/sensors/CarSensorManager.kt index c64ffa2e6ee..78a3fcd0e50 100644 --- a/app/src/main/java/io/homeassistant/companion/android/sensors/CarSensorManager.kt +++ b/app/src/main/java/io/homeassistant/companion/android/sensors/CarSensorManager.kt @@ -25,78 +25,122 @@ class CarSensorManager : SensorManager, DefaultLifecycleObserver { + data class CarSensor( + val sensor: SensorManager.BasicSensor, + val autoEnabled: Boolean = true, + val automotiveEnabled: Boolean = true, + val autoPermissions: List = emptyList(), + /** + * Permissions can be checked here: + * [PropertyUtils.java](https://github.com/androidx/androidx/blob/androidx-main/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java) + */ + val automotivePermissions: List = emptyList() + ) + companion object { internal const val TAG = "CarSM" - private val fuelLevel = SensorManager.BasicSensor( - "car_fuel", - "sensor", - R.string.basic_sensor_name_car_fuel, - R.string.sensor_description_car_fuel, - "mdi:barrel", - unitOfMeasurement = "%", - stateClass = SensorManager.STATE_CLASS_MEASUREMENT, - deviceClass = "battery" + private val fuelLevel = CarSensor( + SensorManager.BasicSensor( + "car_fuel", + "sensor", + R.string.basic_sensor_name_car_fuel, + R.string.sensor_description_car_fuel, + "mdi:barrel", + unitOfMeasurement = "%", + stateClass = SensorManager.STATE_CLASS_MEASUREMENT, + deviceClass = "battery" + ), + autoPermissions = listOf("com.google.android.gms.permission.CAR_FUEL"), + automotivePermissions = listOf( + "android.car.permission.CAR_ENERGY", + "android.car.permission.CAR_ENERGY_PORTS", + "android.car.permission.READ_CAR_DISPLAY_UNITS" + ) ) - private val batteryLevel = SensorManager.BasicSensor( - "car_battery", - "sensor", - R.string.basic_sensor_name_car_battery, - R.string.sensor_description_car_battery, - "mdi:car-battery", - unitOfMeasurement = "%", - stateClass = SensorManager.STATE_CLASS_MEASUREMENT, - deviceClass = "battery", - entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + private val batteryLevel = CarSensor( + SensorManager.BasicSensor( + "car_battery", + "sensor", + R.string.basic_sensor_name_car_battery, + R.string.sensor_description_car_battery, + "mdi:car-battery", + unitOfMeasurement = "%", + stateClass = SensorManager.STATE_CLASS_MEASUREMENT, + deviceClass = "battery", + entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + ), + autoPermissions = listOf("com.google.android.gms.permission.CAR_FUEL"), + automotivePermissions = listOf( + "android.car.permission.CAR_ENERGY", + "android.car.permission.CAR_ENERGY_PORTS", + "android.car.permission.READ_CAR_DISPLAY_UNITS" + ) ) - private val carName = SensorManager.BasicSensor( - "car_name", - "sensor", - R.string.basic_sensor_name_car_name, - R.string.sensor_description_car_name, - "mdi:car-info" + private val carName = CarSensor( + SensorManager.BasicSensor( + "car_name", + "sensor", + R.string.basic_sensor_name_car_name, + R.string.sensor_description_car_name, + "mdi:car-info" + ), + automotivePermissions = listOf("android.car.permission.CAR_INFO") ) - private val carStatus = SensorManager.BasicSensor( - "car_charging_status", - "sensor", - R.string.basic_sensor_name_car_charging_status, - R.string.sensor_description_car_charging_status, - "mdi:ev-station", - deviceClass = "plug" + private val carChargingStatus = CarSensor( + SensorManager.BasicSensor( + "car_charging_status", + "sensor", + R.string.basic_sensor_name_car_charging_status, + R.string.sensor_description_car_charging_status, + "mdi:ev-station", + deviceClass = "plug" + ), + automotivePermissions = listOf("android.car.permission.CAR_ENERGY_PORTS") ) - private val odometerValue = SensorManager.BasicSensor( - "car_odometer", - "sensor", - R.string.basic_sensor_name_car_odometer, - R.string.sensor_description_car_odometer, - "mdi:map-marker-distance", - unitOfMeasurement = "m", - stateClass = SensorManager.STATE_CLASS_TOTAL_INCREASING, - deviceClass = "distance" + private val odometerValue = CarSensor( + SensorManager.BasicSensor( + "car_odometer", + "sensor", + R.string.basic_sensor_name_car_odometer, + R.string.sensor_description_car_odometer, + "mdi:map-marker-distance", + unitOfMeasurement = "m", + stateClass = SensorManager.STATE_CLASS_TOTAL_INCREASING, + deviceClass = "distance" + ), + automotiveEnabled = false, + autoPermissions = listOf("com.google.android.gms.permission.CAR_MILEAGE") ) - - private val fuelType = SensorManager.BasicSensor( - "car_fuel_type", - "sensor", - R.string.basic_sensor_name_car_fuel_type, - R.string.sensor_description_car_fuel_type, - "mdi:gas-station", - entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + private val fuelType = CarSensor( + SensorManager.BasicSensor( + "car_fuel_type", + "sensor", + R.string.basic_sensor_name_car_fuel_type, + R.string.sensor_description_car_fuel_type, + "mdi:gas-station", + entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + ), + autoPermissions = listOf("com.google.android.gms.permission.CAR_FUEL"), + automotivePermissions = listOf("android.car.permission.CAR_INFO") ) - - private val evConnector = SensorManager.BasicSensor( - "car_ev_connector", - "sensor", - R.string.basic_sensor_name_car_ev_connector_type, - R.string.sensor_description_car_ev_connector_type, - "mdi:car-electric", - entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + private val evConnector = CarSensor( + SensorManager.BasicSensor( + "car_ev_connector", + "sensor", + R.string.basic_sensor_name_car_ev_connector_type, + R.string.sensor_description_car_ev_connector_type, + "mdi:car-electric", + entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC + ), + autoPermissions = listOf("com.google.android.gms.permission.CAR_FUEL"), + automotivePermissions = listOf("android.car.permission.CAR_INFO") ) - private val sensorsList = listOf( + private val allSensorsList = listOf( batteryLevel, carName, - carStatus, + carChargingStatus, evConnector, fuelLevel, fuelType, @@ -110,7 +154,7 @@ class CarSensorManager : private val listenerSensors = mapOf( Listener.ENERGY to listOf(batteryLevel, fuelLevel), Listener.MODEL to listOf(carName), - Listener.STATUS to listOf(carStatus), + Listener.STATUS to listOf(carChargingStatus), Listener.MILEAGE to listOf(odometerValue), Listener.PROFILE to listOf(evConnector, fuelType) ) @@ -123,10 +167,23 @@ class CarSensorManager : ) } + private lateinit var latestContext: Context + + private val isAutomotive get() = latestContext.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) + + private val carSensorsList get() = allSensorsList.filter { (isAutomotive && it.automotiveEnabled) || (!isAutomotive && it.autoEnabled) } + private val sensorsList get() = carSensorsList.map { it.sensor } + + private fun allDisabled(): Boolean = sensorsList.none { isEnabled(latestContext, it) } + + private fun connected(): Boolean = HaCarAppService.carInfo != null + override val name: Int get() = R.string.sensor_name_car override suspend fun getAvailableSensors(context: Context): List { + this.latestContext = context.applicationContext + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sensorsList } else { @@ -135,33 +192,39 @@ class CarSensorManager : } override fun hasSensor(context: Context): Boolean { - // TODO: show sensors for automotive (except odometer) once - // we can ask for special automotive permissions in requiredPermissions - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && - !context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) && - BuildConfig.FLAVOR == "full" + this.latestContext = context.applicationContext + + return if (isAutomotive) { + BuildConfig.FLAVOR == "minimal" + } else { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + BuildConfig.FLAVOR == "full" + } } override fun requiredPermissions(sensorId: String): Array { - return when { - (sensorId == fuelLevel.id || sensorId == batteryLevel.id || sensorId == fuelType.id || sensorId == evConnector.id) -> { - arrayOf("com.google.android.gms.permission.CAR_FUEL") - } - sensorId == odometerValue.id -> { - arrayOf("com.google.android.gms.permission.CAR_MILEAGE") + return carSensorsList.firstOrNull { it.sensor.id == sensorId }?.let { + if (isAutomotive) { + it.automotivePermissions.toTypedArray() + } else { + it.autoPermissions.toTypedArray() } - else -> emptyArray() - } + } ?: emptyArray() } - private lateinit var context: Context + fun isEnabled(context: Context, carSensor: CarSensor): Boolean { + this.latestContext = context.applicationContext - private fun allDisabled(): Boolean = sensorsList.none { isEnabled(context, it) } + if ((isAutomotive && !carSensor.automotiveEnabled) || (!isAutomotive && !carSensor.autoEnabled)) { + return false + } - private fun connected(): Boolean = HaCarAppService.carInfo != null + return super.isEnabled(context, carSensor.sensor) + } override fun requestSensorUpdate(context: Context) { - this.context = context.applicationContext + this.latestContext = context.applicationContext + if (allDisabled()) { return } @@ -170,13 +233,13 @@ class CarSensorManager : if (connected()) { updateCarInfo() } else { - sensorsList.forEach { + carSensorsList.forEach { if (isEnabled(context, it)) { onSensorUpdated( context, - it, + it.sensor, STATE_UNAVAILABLE, - it.statelessIcon, + it.sensor.statelessIcon, mapOf() ) } @@ -195,7 +258,7 @@ class CarSensorManager : Log.d(TAG, "unregistering CarInfo $l listener") } - val executor = ContextCompat.getMainExecutor(context) + val executor = ContextCompat.getMainExecutor(latestContext) when (l) { Listener.ENERGY -> { if (enable) { @@ -239,7 +302,7 @@ class CarSensorManager : private fun updateCarInfo() { listenerSensors.forEach { (listener, sensors) -> - if (sensors.any { isEnabled(context, it) }) { + if (sensors.any { isEnabled(latestContext, it) }) { if (listenerLastRegistered[listener] != -1L && listenerLastRegistered[listener]!! + SensorManager.SENSOR_LISTENER_TIMEOUT < System.currentTimeMillis()) { Log.d(TAG, "Re-registering CarInfo $listener listener as it appears to be stuck") setListener(listener, false) @@ -255,12 +318,12 @@ class CarSensorManager : private fun onEnergyAvailable(data: EnergyLevel) { val fuelStatus = carValueStatus(data.fuelPercent.status) Log.d(TAG, "Received Energy level: $data") - if (isEnabled(context, fuelLevel)) { + if (isEnabled(latestContext, fuelLevel)) { onSensorUpdated( - context, - fuelLevel, + latestContext, + fuelLevel.sensor, if (fuelStatus == "success") data.fuelPercent.value!! else STATE_UNKNOWN, - fuelLevel.statelessIcon, + fuelLevel.sensor.statelessIcon, mapOf( "status" to fuelStatus ), @@ -268,12 +331,12 @@ class CarSensorManager : ) } val batteryStatus = carValueStatus(data.batteryPercent.status) - if (isEnabled(context, batteryLevel)) { + if (isEnabled(latestContext, batteryLevel)) { onSensorUpdated( - context, - batteryLevel, + latestContext, + batteryLevel.sensor, if (batteryStatus == "success") data.batteryPercent.value!! else STATE_UNKNOWN, - batteryLevel.statelessIcon, + batteryLevel.sensor.statelessIcon, mapOf( "status" to batteryStatus ), @@ -286,12 +349,12 @@ class CarSensorManager : private fun onModelAvailable(data: Model) { val status = carValueStatus(data.name.status) Log.d(TAG, "Received model information: $data") - if (isEnabled(context, carName)) { + if (isEnabled(latestContext, carName)) { onSensorUpdated( - context, - carName, + latestContext, + carName.sensor, if (status == "success") data.name.value!! else STATE_UNKNOWN, - carName.statelessIcon, + carName.sensor.statelessIcon, mapOf( "car_manufacturer" to data.manufacturer.value, "car_manufactured_year" to data.year.value, @@ -307,12 +370,12 @@ class CarSensorManager : fun onStatusAvailable(data: EvStatus) { val status = carValueStatus(data.evChargePortConnected.status) Log.d(TAG, "Received status available: $data") - if (isEnabled(context, carStatus)) { + if (isEnabled(latestContext, carChargingStatus)) { onSensorUpdated( - context, - carStatus, + latestContext, + carChargingStatus.sensor, if (status == "success") (data.evChargePortConnected.value == true) else STATE_UNKNOWN, - carStatus.statelessIcon, + carChargingStatus.sensor.statelessIcon, mapOf( "car_charge_port_open" to (data.evChargePortOpen.value == true), "status" to status @@ -327,12 +390,12 @@ class CarSensorManager : fun onMileageAvailable(data: Mileage) { val status = carValueStatus(data.odometerMeters.status) Log.d(TAG, "Received mileage: $data") - if (isEnabled(context, odometerValue)) { + if (isEnabled(latestContext, odometerValue)) { onSensorUpdated( - context, - odometerValue, + latestContext, + odometerValue.sensor, if (status == "success") data.odometerMeters.value!! else STATE_UNKNOWN, - odometerValue.statelessIcon, + odometerValue.sensor.statelessIcon, mapOf( "status" to status ), @@ -346,24 +409,24 @@ class CarSensorManager : val fuelTypeStatus = carValueStatus(data.fuelTypes.status) val evConnectorTypeStatus = carValueStatus(data.evConnectorTypes.status) Log.d(TAG, "Received energy profile: $data") - if (isEnabled(context, fuelType)) { + if (isEnabled(latestContext, fuelType)) { onSensorUpdated( - context, - fuelType, + latestContext, + fuelType.sensor, if (fuelTypeStatus == "success") getFuelType(data.fuelTypes.value!!) else STATE_UNKNOWN, - fuelType.statelessIcon, + fuelType.sensor.statelessIcon, mapOf( "status" to fuelTypeStatus ), forceUpdate = true ) } - if (isEnabled(context, evConnector)) { + if (isEnabled(latestContext, evConnector)) { onSensorUpdated( - context, - evConnector, + latestContext, + evConnector.sensor, if (evConnectorTypeStatus == "success") getEvConnectorType(data.evConnectorTypes.value!!) else STATE_UNKNOWN, - evConnector.statelessIcon, + evConnector.sensor.statelessIcon, mapOf( "status" to evConnectorTypeStatus ), diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index 747b851bbd1..7d9e53f659b 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -44,6 +44,10 @@ + + + +