Skip to content
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
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<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,
Expand All @@ -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)
)
Expand All @@ -123,10 +167,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
Copy link
Contributor Author

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

Copy link
Member

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

Copy link
Contributor Author

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


return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sensorsList
} else {
Expand All @@ -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.context = context.applicationContext

return if (isAutomotive) {
BuildConfig.FLAVOR == "minimal"
dshokouhi marked this conversation as resolved.
Show resolved Hide resolved
} else {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
BuildConfig.FLAVOR == "full"
}
}

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) {
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Expand All @@ -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()
)
}
Expand Down Expand Up @@ -258,9 +321,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
),
Expand All @@ -271,9 +334,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
),
Expand All @@ -289,9 +352,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,
Expand All @@ -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(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
Expand All @@ -330,9 +393,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
),
Expand All @@ -349,9 +412,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
),
Expand All @@ -361,9 +424,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
),
Expand Down
4 changes: 4 additions & 0 deletions automotive/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.car.permission.CAR_INFO" />
<uses-permission android:name="android.car.permission.CAR_ENERGY" />
<uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS" />

<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.telephony" android:required="false"/>
Expand Down