-
Notifications
You must be signed in to change notification settings - Fork 629
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate car sensors with automotive, and added permissions #4122
Merged
JBassett
merged 7 commits into
home-assistant:master
from
ivancea:feat/automotive-enable-sensors
Jan 24, 2024
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c6c8695
Integrate car sensors with automotive, and added permissions
ivancea 7f6e99a
Set context in every call
ivancea cf29d95
Fixed CarSensorManager format
ivancea d74572f
Merge branch 'master' into feat/automotive-enable-sensors
ivancea 361f81a
Enable car sensors only on correct flavors
ivancea 36d7507
Fixed format
ivancea 0846cb8
Renamed context to latestContext and added parenthesis to expression
ivancea File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,6 @@ import androidx.car.app.hardware.info.Mileage | |
import androidx.car.app.hardware.info.Model | ||
import androidx.core.content.ContextCompat | ||
import androidx.lifecycle.DefaultLifecycleObserver | ||
import io.homeassistant.companion.android.BuildConfig | ||
import io.homeassistant.companion.android.common.R | ||
import io.homeassistant.companion.android.common.sensors.SensorManager | ||
import io.homeassistant.companion.android.common.util.STATE_UNAVAILABLE | ||
|
@@ -25,78 +24,122 @@ class CarSensorManager : | |
SensorManager, | ||
DefaultLifecycleObserver { | ||
|
||
data class CarSensor( | ||
val sensor : SensorManager.BasicSensor, | ||
val autoEnabled : Boolean = true, | ||
val automotiveEnabled : Boolean = true, | ||
val autoPermissions : List<String> = 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<String> = 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 +153,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 +166,23 @@ class CarSensorManager : | |
) | ||
} | ||
|
||
private lateinit var context: Context | ||
|
||
private val isAutomotive get() = context.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(context, 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<SensorManager.BasicSensor> { | ||
this.context = context.applicationContext | ||
|
||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
sensorsList | ||
} else { | ||
|
@@ -135,33 +191,34 @@ 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.context = context.applicationContext | ||
|
||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O | ||
ivancea marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
override fun requiredPermissions(sensorId: String): Array<String> { | ||
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.context = context.applicationContext | ||
|
||
private fun allDisabled(): Boolean = sensorsList.none { isEnabled(context, it) } | ||
if (isAutomotive && !carSensor.automotiveEnabled || !isAutomotive && !carSensor.autoEnabled) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to add some parenthesis here so the conditions are properly evaluated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding them to improve readability |
||
return false | ||
} | ||
|
||
private fun connected(): Boolean = HaCarAppService.carInfo != null | ||
return super.isEnabled(context, carSensor.sensor) | ||
} | ||
|
||
override fun requestSensorUpdate(context: Context) { | ||
this.context = context.applicationContext | ||
|
||
if (allDisabled()) { | ||
return | ||
} | ||
|
@@ -170,13 +227,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() | ||
) | ||
} | ||
|
@@ -258,9 +315,9 @@ class CarSensorManager : | |
if (isEnabled(context, fuelLevel)) { | ||
onSensorUpdated( | ||
context, | ||
fuelLevel, | ||
fuelLevel.sensor, | ||
if (fuelStatus == "success") data.fuelPercent.value!! else STATE_UNKNOWN, | ||
fuelLevel.statelessIcon, | ||
fuelLevel.sensor.statelessIcon, | ||
mapOf( | ||
"status" to fuelStatus | ||
), | ||
|
@@ -271,9 +328,9 @@ class CarSensorManager : | |
if (isEnabled(context, batteryLevel)) { | ||
onSensorUpdated( | ||
context, | ||
batteryLevel, | ||
batteryLevel.sensor, | ||
if (batteryStatus == "success") data.batteryPercent.value!! else STATE_UNKNOWN, | ||
batteryLevel.statelessIcon, | ||
batteryLevel.sensor.statelessIcon, | ||
mapOf( | ||
"status" to batteryStatus | ||
), | ||
|
@@ -289,9 +346,9 @@ class CarSensorManager : | |
if (isEnabled(context, carName)) { | ||
onSensorUpdated( | ||
context, | ||
carName, | ||
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 +364,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(context, carChargingStatus)) { | ||
onSensorUpdated( | ||
context, | ||
carStatus, | ||
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 | ||
|
@@ -330,9 +387,9 @@ class CarSensorManager : | |
if (isEnabled(context, odometerValue)) { | ||
onSensorUpdated( | ||
context, | ||
odometerValue, | ||
odometerValue.sensor, | ||
if (status == "success") data.odometerMeters.value!! else STATE_UNKNOWN, | ||
odometerValue.statelessIcon, | ||
odometerValue.sensor.statelessIcon, | ||
mapOf( | ||
"status" to status | ||
), | ||
|
@@ -349,9 +406,9 @@ class CarSensorManager : | |
if (isEnabled(context, fuelType)) { | ||
onSensorUpdated( | ||
context, | ||
fuelType, | ||
fuelType.sensor, | ||
if (fuelTypeStatus == "success") getFuelType(data.fuelTypes.value!!) else STATE_UNKNOWN, | ||
fuelType.statelessIcon, | ||
fuelType.sensor.statelessIcon, | ||
mapOf( | ||
"status" to fuelTypeStatus | ||
), | ||
|
@@ -361,9 +418,9 @@ class CarSensorManager : | |
if (isEnabled(context, evConnector)) { | ||
onSensorUpdated( | ||
context, | ||
evConnector, | ||
evConnector.sensor, | ||
if (evConnectorTypeStatus == "success") getEvConnectorType(data.evConnectorTypes.value!!) else STATE_UNKNOWN, | ||
evConnector.statelessIcon, | ||
evConnector.sensor.statelessIcon, | ||
mapOf( | ||
"status" to evConnectorTypeStatus | ||
), | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those lateinit context set are a bit sketchy. I had to set it from more methods, as it wasn't set in some calls. I added it to every method accepting a context, just in case it happens again in the future.
I'm considering 2 other options: Adding it on construction (Not sure if possible), or simply drilling it through method calls, so we're sure we actually have a context when calling something that needs it.
I'd like to see more opinions about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we do actually use a
latestContext
method of storing the latest context received in the app for some sensors, very similar to what you are doing here.https://github.com/home-assistant/android/blob/master/common/src/main/java/io/homeassistant/companion/android/common/sensors/LightSensorManager.kt#L57
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Renaming to latestContext, to keep the same naming as the other sensors