[go: nahoru, domu]

blob: 7712336cd63acf375ff5d3bd94ab0790a8a2c1db [file] [log] [blame]
/*
* Copyright 2021 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 androidx.car.app.hardware.info;
import static android.car.VehiclePropertyIds.DISTANCE_DISPLAY_UNITS;
import static android.car.VehiclePropertyIds.EV_BATTERY_LEVEL;
import static android.car.VehiclePropertyIds.FUEL_LEVEL;
import static android.car.VehiclePropertyIds.FUEL_LEVEL_LOW;
import static android.car.VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY;
import static android.car.VehiclePropertyIds.INFO_EV_CONNECTOR_TYPE;
import static android.car.VehiclePropertyIds.INFO_FUEL_CAPACITY;
import static android.car.VehiclePropertyIds.INFO_FUEL_TYPE;
import static android.car.VehiclePropertyIds.INFO_MAKE;
import static android.car.VehiclePropertyIds.INFO_MODEL;
import static android.car.VehiclePropertyIds.INFO_MODEL_YEAR;
import static android.car.VehiclePropertyIds.PERF_ODOMETER;
import static android.car.VehiclePropertyIds.RANGE_REMAINING;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static java.util.Objects.requireNonNull;
import android.car.VehiclePropertyIds;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.hardware.common.CarPropertyResponse;
import androidx.car.app.hardware.common.CarValue;
import androidx.car.app.hardware.common.GetPropertyRequest;
import androidx.car.app.hardware.common.OnCarDataAvailableListener;
import androidx.car.app.hardware.common.OnCarPropertyResponseListener;
import androidx.car.app.hardware.common.PropertyManager;
import androidx.car.app.hardware.common.PropertyUtils;
import androidx.car.app.utils.LogTags;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
* Manages access to vehicle specific info, for example, energy info, model info.
*
* @hide
*/
@RestrictTo(LIBRARY)
public class AutomotiveCarInfo implements CarInfo {
@VisibleForTesting
static final List<Integer> ENERGY_LEVEL_REQUEST =
Arrays.asList(EV_BATTERY_LEVEL, FUEL_LEVEL,
FUEL_LEVEL_LOW, RANGE_REMAINING,
DISTANCE_DISPLAY_UNITS);
@VisibleForTesting
static final float DEFAULT_SAMPLE_RATE = 5f;
/*
* ELECTRONIC_TOLL_COLLECTION_CARD_STATUS in VehiclePropertyIds. The property is added after
* Android Q.
*/
public static final int TOLL_CARD_STATUS_ID = 289410874;
// VEHICLE_SPEED_DISPLAY_UNIT in VehiclePropertyIds. The property is added after Android Q.
public static final int SPEED_DISPLAY_UNIT_ID = 289408516;
private static final float UNKNOWN_CAPACITY = Float.NEGATIVE_INFINITY;
private static final List<Integer> MILEAGE_REQUEST =
Arrays.asList(PERF_ODOMETER, DISTANCE_DISPLAY_UNITS);
static final List<Integer> TOLL_REQUEST =
Collections.singletonList(TOLL_CARD_STATUS_ID);
private static final List<Integer> SPEED_REQUEST =
Arrays.asList(VehiclePropertyIds.PERF_VEHICLE_SPEED,
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY, SPEED_DISPLAY_UNIT_ID);
private final Map<OnCarDataAvailableListener<?>, OnCarPropertyResponseListener> mListenerMap =
new HashMap<>();
private final PropertyManager mPropertyManager;
/**
* AutomotiveCarInfo class constructor initializing PropertyWorkManager object.
*
* @throws NullPointerException if {@code manager} is null
*/
public AutomotiveCarInfo(@NonNull PropertyManager manager) {
mPropertyManager = requireNonNull(manager);
}
@Override
public void fetchModel(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<Model> listener) {
// Prepare request GetPropertyRequest
List<GetPropertyRequest> request = new ArrayList<>();
// Add "make", "model", "year" of the vehicle to the requests.
request.add(GetPropertyRequest.create(INFO_MAKE));
request.add(GetPropertyRequest.create(INFO_MODEL));
request.add(GetPropertyRequest.create(INFO_MODEL_YEAR));
ListenableFuture<List<CarPropertyResponse<?>>> future =
mPropertyManager.submitGetPropertyRequest(request, executor);
populateModelData(executor, listener, future);
}
@Override
public void fetchEnergyProfile(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<EnergyProfile> listener) {
// Prepare request GetPropertyRequest
List<GetPropertyRequest> request = new ArrayList<>();
// Add "evConnector" and "fuel" type of the vehicle to the requests.
request.add(GetPropertyRequest.create(INFO_EV_CONNECTOR_TYPE));
request.add(GetPropertyRequest.create(INFO_FUEL_TYPE));
ListenableFuture<List<CarPropertyResponse<?>>> future =
mPropertyManager.submitGetPropertyRequest(request, executor);
populateEnergyProfileData(executor, listener, future);
}
@Override
public void addTollListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<TollCard> listener) {
if (Build.VERSION.SDK_INT > 30) {
Api31Impl.addTollListener(executor, listener, mPropertyManager, mListenerMap);
} else {
TollCard unimplementedTollCard = new TollCard.Builder()
.setCardState(CarValue.UNIMPLEMENTED_INTEGER).build();
executor.execute(() -> listener.onCarDataAvailable(unimplementedTollCard));
}
}
@Override
public void removeTollListener(@NonNull OnCarDataAvailableListener<TollCard> listener) {
OnCarPropertyResponseListener responseListener = mListenerMap.remove(listener);
if (responseListener == null) {
return;
}
if (Build.VERSION.SDK_INT > 30) {
Api31Impl.removeTollListener(responseListener, mPropertyManager);
}
}
@Override
public void addEnergyLevelListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<EnergyLevel> listener) {
getCapacitiesThenEnergyLevel(executor, listener);
}
@Override
public void removeEnergyLevelListener(
@NonNull OnCarDataAvailableListener<EnergyLevel> listener) {
removeListenerImpl(listener);
}
@Override
public void addSpeedListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<Speed> listener) {
SpeedListener speedListener = new SpeedListener(listener, executor);
mPropertyManager.submitRegisterListenerRequest(SPEED_REQUEST, DEFAULT_SAMPLE_RATE,
speedListener, executor);
mListenerMap.put(listener, speedListener);
}
@Override
public void removeSpeedListener(@NonNull OnCarDataAvailableListener<Speed> listener) {
removeListenerImpl(listener);
}
@Override
public void addMileageListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<Mileage> listener) {
MileageListener mileageListener = new MileageListener(listener, executor);
mPropertyManager.submitRegisterListenerRequest(MILEAGE_REQUEST, DEFAULT_SAMPLE_RATE,
mileageListener, executor);
mListenerMap.put(listener, mileageListener);
}
@Override
public void removeMileageListener(@NonNull OnCarDataAvailableListener<Mileage> listener) {
removeListenerImpl(listener);
}
static <T> CarValue<T> getCarValue(CarPropertyResponse<?> response, @Nullable T value) {
long timestampMillis = response.getTimestampMillis();
int status = response.getStatus();
return new CarValue<>(value, timestampMillis, status);
}
@VisibleForTesting
void getCapacitiesThenEnergyLevel(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<EnergyLevel> listener) {
// Prepare request GetPropertyRequest for battery and fuel capacities.
List<GetPropertyRequest> request = new ArrayList<>();
// Add "evConnector" and "fuel" type of the vehicle to the requests.
request.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
request.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
ListenableFuture<List<CarPropertyResponse<?>>> capacityFuture =
mPropertyManager.submitGetPropertyRequest(request, executor);
EnergyLevelListener energyLevelListener = new EnergyLevelListener(listener, executor);
// This future will get EV battery capacity and fuel capacity for calculating the
// percentage of battery level and fuel level. Without those values, we still can provide
// fuel_level_low, distance_units and range_remaining information in EnergyLevelListener.
capacityFuture.addListener(() -> {
try {
float evBatteryCapacity = UNKNOWN_CAPACITY;
float fuelCapacity = UNKNOWN_CAPACITY;
List<CarPropertyResponse<?>> result = capacityFuture.get();
for (CarPropertyResponse<?> value : result) {
if (value.getValue() == null) {
Log.w(LogTags.TAG_CAR_HARDWARE,
"Failed to retrieve CarPropertyResponse value for property id "
+ value.getPropertyId());
continue;
}
if (value.getPropertyId() == INFO_EV_BATTERY_CAPACITY
&& value.getStatus() == CarValue.STATUS_SUCCESS) {
evBatteryCapacity = (Float) value.getValue();
energyLevelListener.updateEvBatteryCapacity(evBatteryCapacity);
}
if (value.getPropertyId() == INFO_FUEL_CAPACITY
&& value.getStatus() == CarValue.STATUS_SUCCESS) {
fuelCapacity = (Float) value.getValue();
energyLevelListener.updateFuelCapacity(fuelCapacity);
}
}
} catch (ExecutionException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Energy Level", e);
} catch (InterruptedException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Energy Level", e);
Thread.currentThread().interrupt();
}
}, executor);
mPropertyManager.submitRegisterListenerRequest(ENERGY_LEVEL_REQUEST,
DEFAULT_SAMPLE_RATE, energyLevelListener, executor);
mListenerMap.put(listener, energyLevelListener);
}
private void populateModelData(@NonNull Executor executor,
OnCarDataAvailableListener<Model> listener,
ListenableFuture<List<CarPropertyResponse<?>>> future) {
future.addListener(() -> {
try {
List<CarPropertyResponse<?>> result = future.get();
CarValue<String> makeValue = CarValue.UNIMPLEMENTED_STRING;
CarValue<Integer> yearValue = CarValue.UNIMPLEMENTED_INTEGER;
CarValue<String> modelValue = CarValue.UNIMPLEMENTED_STRING;
for (CarPropertyResponse<?> value : result) {
if (value.getValue() == null) {
Log.w(LogTags.TAG_CAR_HARDWARE,
"Failed to retrieve CarPropertyResponse value for property id "
+ value.getPropertyId());
continue;
}
if (value.getPropertyId() == INFO_MAKE) {
makeValue = getCarValue(value, (String) value.getValue());
}
if (value.getPropertyId() == INFO_MODEL) {
modelValue = getCarValue(value, (String) value.getValue());
}
if (value.getPropertyId() == INFO_MODEL_YEAR) {
yearValue = getCarValue(value, (Integer) value.getValue());
}
}
Model model = new Model.Builder().setName(modelValue)
.setManufacturer(makeValue)
.setYear(yearValue)
.build();
listener.onCarDataAvailable(model);
} catch (ExecutionException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Model", e);
} catch (InterruptedException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Model", e);
Thread.currentThread().interrupt();
}
}, executor);
}
private void populateEnergyProfileData(@NonNull Executor executor,
OnCarDataAvailableListener<EnergyProfile> listener,
ListenableFuture<List<CarPropertyResponse<?>>> future) {
future.addListener(() -> {
try {
List<CarPropertyResponse<?>> result = future.get();
CarValue<List<Integer>> evConnector = CarValue.UNIMPLEMENTED_INTEGER_LIST;
CarValue<List<Integer>> fuel = CarValue.UNIMPLEMENTED_INTEGER_LIST;
for (CarPropertyResponse<?> value : result) {
if (value.getValue() == null) {
Log.w(LogTags.TAG_CAR_HARDWARE,
"Failed to retrieve CarPropertyResponse value for property id"
+ value.getPropertyId());
continue;
}
if (value.getPropertyId() == INFO_EV_CONNECTOR_TYPE) {
Integer[] evConnectorsInVehicle = (Integer[]) value.getValue();
List<Integer> evConnectorsInCarValue = new ArrayList<>();
for (Integer connectorType : evConnectorsInVehicle) {
evConnectorsInCarValue.add(
PropertyUtils.covertEvConnectorType(connectorType));
}
evConnector = getCarValue(value, evConnectorsInCarValue);
}
if (value.getPropertyId() == INFO_FUEL_TYPE) {
fuel = getCarValue(value, Arrays.stream((Integer[]) requireNonNull(
value.getValue())).collect(Collectors.toList()));
}
}
EnergyProfile energyProfile = new EnergyProfile.Builder().setEvConnectorTypes(
evConnector)
.setFuelTypes(fuel)
.build();
listener.onCarDataAvailable(energyProfile);
} catch (ExecutionException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Energy Profile", e);
} catch (InterruptedException e) {
Log.e(LogTags.TAG_CAR_HARDWARE,
"Failed to get CarPropertyResponse for Energy Profile", e);
Thread.currentThread().interrupt();
}
}, executor);
}
@RequiresApi(31)
private static class Api31Impl {
@DoNotInline
static void addTollListener(Executor executor,
OnCarDataAvailableListener<TollCard> listener, PropertyManager propertyManager,
Map<OnCarDataAvailableListener<?>, OnCarPropertyResponseListener> listenerMap) {
TollListener tollListener = new TollListener(listener, executor);
propertyManager.submitRegisterListenerRequest(TOLL_REQUEST, DEFAULT_SAMPLE_RATE,
tollListener, executor);
listenerMap.put(listener, tollListener);
}
@DoNotInline
static void removeTollListener(OnCarPropertyResponseListener listener,
PropertyManager propertyManager) {
propertyManager.submitUnregisterListenerRequest(listener);
}
}
private void removeListenerImpl(OnCarDataAvailableListener<?> listener) {
OnCarPropertyResponseListener responseListener = mListenerMap.remove(listener);
if (responseListener != null) {
mPropertyManager.submitUnregisterListenerRequest(responseListener);
}
}
private static class TollListener implements OnCarPropertyResponseListener {
private final OnCarDataAvailableListener<TollCard> mTollOnCarDataListener;
private final Executor mExecutor;
TollListener(OnCarDataAvailableListener<TollCard> listener, Executor executor) {
mTollOnCarDataListener = listener;
mExecutor = executor;
}
@Override
public void onCarPropertyResponses(
@NonNull List<CarPropertyResponse<?>> carPropertyResponses) {
if (carPropertyResponses.size() != 1
|| carPropertyResponses.get(0).getPropertyId() != TOLL_CARD_STATUS_ID) {
Log.e(LogTags.TAG_CAR_HARDWARE, "Invalid response callback in TollListener");
return;
}
mExecutor.execute(() -> {
CarPropertyResponse<?> response = carPropertyResponses.get(0);
CarValue<Integer> tollValue = getCarValue(response, (Integer) response.getValue());
TollCard toll = new TollCard.Builder().setCardState(tollValue).build();
mTollOnCarDataListener.onCarDataAvailable(toll);
});
}
}
private static class SpeedListener implements OnCarPropertyResponseListener {
private final OnCarDataAvailableListener<Speed> mSpeedOnCarDataListener;
private final Executor mExecutor;
SpeedListener(OnCarDataAvailableListener<Speed> listener, Executor executor) {
mSpeedOnCarDataListener = listener;
mExecutor = executor;
}
@Override
public void onCarPropertyResponses(
@NonNull List<CarPropertyResponse<?>> carPropertyResponses) {
if (carPropertyResponses.size() != 3) {
Log.e(LogTags.TAG_CAR_HARDWARE, "Invalid response callback in SpeedListener.");
return;
}
mExecutor.execute(() -> {
CarValue<Float> rawSpeedValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Float> displaySpeedValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Integer> displayUnitValue = CarValue.UNIMPLEMENTED_INTEGER;
for (CarPropertyResponse<?> response : carPropertyResponses) {
switch (response.getPropertyId()) {
case VehiclePropertyIds.PERF_VEHICLE_SPEED:
rawSpeedValue = getCarValue(response, (Float) response.getValue());
break;
case VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY:
displaySpeedValue = getCarValue(response, (Float) response.getValue());
break;
case SPEED_DISPLAY_UNIT_ID:
Integer speedUnit = null;
if (response.getValue() != null) {
speedUnit = PropertyUtils.covertSpeedUnit(
(Integer) response.getValue());
}
displayUnitValue = getCarValue(response, speedUnit);
break;
default:
Log.e(LogTags.TAG_CAR_HARDWARE,
"Invalid response callback in SpeedListener.");
}
}
Speed speed = new Speed.Builder().setRawSpeedMetersPerSecond(rawSpeedValue)
.setDisplaySpeedMetersPerSecond(displaySpeedValue)
.setSpeedDisplayUnit(displayUnitValue).build();
mSpeedOnCarDataListener.onCarDataAvailable(speed);
});
}
}
/**
* Mileage listener to get distance display unit and odometer updates by
* {@link CarPropertyResponse}.
*/
@VisibleForTesting
static class MileageListener implements OnCarPropertyResponseListener {
private final OnCarDataAvailableListener<Mileage> mMileageOnCarDataAvailableListener;
private final Executor mExecutor;
MileageListener(OnCarDataAvailableListener<Mileage> listener, Executor executor) {
mMileageOnCarDataAvailableListener = listener;
mExecutor = executor;
}
@Override
public void onCarPropertyResponses(
@NonNull List<CarPropertyResponse<?>> carPropertyResponses) {
if (carPropertyResponses.size() == 2) {
mExecutor.execute(() -> {
CarValue<Float> odometerValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Integer> distanceDisplayUnitValue = CarValue.UNIMPLEMENTED_INTEGER;
for (CarPropertyResponse<?> response : carPropertyResponses) {
if (response.getValue() == null) {
Log.w(LogTags.TAG_CAR_HARDWARE,
"Failed to retrieve CarPropertyResponse value for property id "
+ response.getPropertyId());
continue;
}
switch (response.getPropertyId()) {
case PERF_ODOMETER:
odometerValue = new CarValue<>(
(Float) response.getValue(),
response.getTimestampMillis(), response.getStatus());
break;
case DISTANCE_DISPLAY_UNITS:
Integer displayUnit = null;
if (response.getValue() != null) {
displayUnit = PropertyUtils.covertDistanceUnit(
(Integer) response.getValue());
}
distanceDisplayUnitValue = new CarValue<>(displayUnit,
response.getTimestampMillis(), response.getStatus());
break;
default:
Log.e(LogTags.TAG_CAR_HARDWARE,
"Invalid response callback in MileageListener");
}
}
Mileage mileage =
new Mileage.Builder().setOdometerMeters(
odometerValue).setDistanceDisplayUnit(
distanceDisplayUnitValue).build();
mMileageOnCarDataAvailableListener.onCarDataAvailable(mileage);
});
}
}
}
/**
* EnergyLevel listener to get battery, energy updates by {@link CarPropertyResponse}.
*/
@VisibleForTesting
static class EnergyLevelListener implements OnCarPropertyResponseListener {
private final OnCarDataAvailableListener<EnergyLevel>
mEnergyLevelOnCarDataAvailableListener;
private final Executor mExecutor;
private float mEvBatteryCapacity = UNKNOWN_CAPACITY;
private float mFuelCapacity = UNKNOWN_CAPACITY;
EnergyLevelListener(OnCarDataAvailableListener<EnergyLevel> listener, Executor executor) {
mEnergyLevelOnCarDataAvailableListener = listener;
mExecutor = executor;
}
void updateEvBatteryCapacity(float evBatteryCapacity) {
mEvBatteryCapacity = evBatteryCapacity;
}
void updateFuelCapacity(float fuelCapacity) {
mFuelCapacity = fuelCapacity;
}
@Override
public void onCarPropertyResponses(
@NonNull List<CarPropertyResponse<?>> carPropertyResponses) {
if (carPropertyResponses.size() == 5) {
mExecutor.execute(() -> {
CarValue<Float> batteryPercentValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Float> fuelPercentValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Boolean> energyIsLowValue = CarValue.UNIMPLEMENTED_BOOLEAN;
CarValue<Float> rangeRemainingValue = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<Integer> distanceDisplayUnitValue =
CarValue.UNIMPLEMENTED_INTEGER;
for (CarPropertyResponse<?> response : carPropertyResponses) {
if (response.getValue() == null) {
Log.w(LogTags.TAG_CAR_HARDWARE,
"Failed to retrieve CarPropertyResponse value for property id "
+ response.getPropertyId());
continue;
}
switch (response.getPropertyId()) {
case EV_BATTERY_LEVEL:
if (mEvBatteryCapacity != Float.NEGATIVE_INFINITY) {
batteryPercentValue = new CarValue<>(
(Float) response.getValue() / mEvBatteryCapacity * 100,
response.getTimestampMillis(), response.getStatus());
}
break;
case FUEL_LEVEL:
if (mFuelCapacity != Float.NEGATIVE_INFINITY) {
fuelPercentValue = new CarValue<>(
(Float) response.getValue() / mFuelCapacity * 100,
response.getTimestampMillis(), response.getStatus());
}
break;
case FUEL_LEVEL_LOW:
energyIsLowValue = new CarValue<>(
(Boolean) response.getValue(),
response.getTimestampMillis(), response.getStatus());
break;
case RANGE_REMAINING:
rangeRemainingValue = new CarValue<>(
(Float) response.getValue(),
response.getTimestampMillis(), response.getStatus());
break;
case DISTANCE_DISPLAY_UNITS:
distanceDisplayUnitValue = new CarValue<>(
(Integer) response.getValue(),
response.getTimestampMillis(), response.getStatus());
break;
default:
Log.e(LogTags.TAG_CAR_HARDWARE,
"Invalid response callback in EnergyLevelListener");
}
}
EnergyLevel energyLevel =
new EnergyLevel.Builder().setBatteryPercent(
batteryPercentValue).setFuelPercent(
fuelPercentValue).setEnergyIsLow(
energyIsLowValue).setRangeRemainingMeters(
rangeRemainingValue).setDistanceDisplayUnit(
distanceDisplayUnitValue).build();
mEnergyLevelOnCarDataAvailableListener.onCarDataAvailable(energyLevel);
});
}
}
}
}