AutomotiveCarInfo.java

/*
 * 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(makeValue)
                        .setManufacturer(modelValue)
                        .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);
                });
            }
        }
    }
}