blob: 7412ac604c8a5d9a09299fbbaa9508ac58356eda [file] [log] [blame]
/*
* Copyright (C) 2018 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 android.car.cluster;
import android.annotation.Nullable;
import android.app.Application;
import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.CarNotConnectedException;
import android.car.VehicleAreaType;
import android.car.cluster.sensors.Sensor;
import android.car.cluster.sensors.Sensors;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyManager;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* {@link AndroidViewModel} for cluster information.
*/
public class ClusterViewModel extends AndroidViewModel {
private static final String TAG = "Cluster.ViewModel";
private static final int PROPERTIES_REFRESH_RATE_UI = 5;
private float mSpeedFactor;
private float mDistanceFactor;
public enum NavigationActivityState {
/** No activity has been selected to be displayed on the navigation fragment yet */
NOT_SELECTED,
/** An activity has been selected, but it is not yet visible to the user */
LOADING,
/** Navigation activity is visible to the user */
VISIBLE,
}
private ComponentName mFreeNavigationActivity;
private ComponentName mCurrentNavigationActivity;
private final MutableLiveData<NavigationActivityState> mNavigationActivityStateLiveData =
new MutableLiveData<>();
private final MutableLiveData<Boolean> mNavigationFocus = new MutableLiveData<>(false);
private Car mCar;
private CarAppFocusManager mCarAppFocusManager;
private CarPropertyManager mCarPropertyManager;
private Map<Sensor<?>, MutableLiveData<?>> mSensorLiveDatas = new HashMap<>();
private ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
registerAppFocusListener();
registerCarPropertiesListener();
} catch (CarNotConnectedException e) {
Log.e(TAG, "onServiceConnected: error obtaining manager", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "onServiceDisconnected, name: " + name);
mCarAppFocusManager = null;
mCarPropertyManager = null;
}
};
private void registerAppFocusListener() throws CarNotConnectedException {
mCarAppFocusManager = (CarAppFocusManager) mCar.getCarManager(
Car.APP_FOCUS_SERVICE);
if (mCarAppFocusManager != null) {
mCarAppFocusManager.addFocusListener(
(appType, active) -> setNavigationFocus(active),
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
} else {
Log.e(TAG, "onServiceConnected: unable to obtain CarAppFocusManager");
}
}
private void registerCarPropertiesListener() throws CarNotConnectedException {
Sensors sensors = Sensors.getInstance();
mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
for (Integer propertyId : sensors.getPropertyIds()) {
try {
mCarPropertyManager.registerCallback(mCarPropertyEventCallback,
propertyId, PROPERTIES_REFRESH_RATE_UI);
} catch (SecurityException ex) {
Log.e(TAG, "onServiceConnected: Unable to listen to car property: " + propertyId
+ " sensors: " + sensors.getSensorsForPropertyId(propertyId), ex);
}
}
}
private CarPropertyManager.CarPropertyEventCallback mCarPropertyEventCallback =
new CarPropertyManager.CarPropertyEventCallback() {
@Override
public void onChangeEvent(CarPropertyValue value) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"CarProperty change: property " + value.getPropertyId() + ", area"
+ value.getAreaId() + ", value: " + value.getValue());
}
for (Sensor<?> sensorId : Sensors.getInstance()
.getSensorsForPropertyId(value.getPropertyId())) {
if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID
|| (sensorId.mAreaId & value.getAreaId()) != 0) {
setSensorValue(sensorId, value);
}
}
}
@Override
public void onErrorEvent(int propId, int zone) {
for (Sensor<?> sensorId : Sensors.getInstance().getSensorsForPropertyId(
propId)) {
if (sensorId.mAreaId == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL
|| (sensorId.mAreaId & zone) != 0) {
setSensorValue(sensorId, null);
}
}
}
private <T> void setSensorValue(Sensor<T> id, CarPropertyValue<?> value) {
T newValue = value != null ? id.mAdapter.apply(value) : null;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Sensor " + id.mName + " = " + newValue);
}
getSensorMutableLiveData(id).setValue(newValue);
}
};
/**
* New {@link ClusterViewModel} instance
*/
public ClusterViewModel(@NonNull Application application) {
super(application);
mCar = Car.createCar(application, mCarServiceConnection);
mCar.connect();
TypedValue tv = new TypedValue();
getApplication().getResources().getValue(R.dimen.speed_factor, tv, true);
mSpeedFactor = tv.getFloat();
getApplication().getResources().getValue(R.dimen.distance_factor, tv, true);
mDistanceFactor = tv.getFloat();
}
@Override
protected void onCleared() {
super.onCleared();
mCar.disconnect();
mCar = null;
mCarAppFocusManager = null;
mCarPropertyManager = null;
}
/**
* Returns a {@link LiveData} providing the current state of the activity displayed on the
* navigation fragment.
*/
public LiveData<NavigationActivityState> getNavigationActivityState() {
return mNavigationActivityStateLiveData;
}
/**
* Returns a {@link LiveData} indicating whether navigation focus is currently being granted
* or not. This indicates whether a navigation application is currently providing driving
* directions.
*/
public LiveData<Boolean> getNavigationFocus() {
return mNavigationFocus;
}
/**
* Returns a {@link LiveData} that tracks the value of a given car sensor. Each sensor has its
* own data type. The list of all supported sensors can be found at {@link Sensors}
*
* @param sensor sensor to observe
* @param <T> data type of such sensor
*/
@SuppressWarnings("unchecked")
@NonNull
public <T> LiveData<T> getSensor(@NonNull Sensor<T> sensor) {
return getSensorMutableLiveData(Preconditions.checkNotNull(sensor));
}
/**
* Returns the current value of the sensor, directly from the VHAL.
*
* @param sensor sensor to read
* @param <V> VHAL data type
* @param <T> data type of such sensor
*/
@Nullable
public <T> T getSensorValue(@NonNull Sensor<T> sensor) {
try {
CarPropertyValue<?> value = mCarPropertyManager
.getProperty(sensor.mPropertyId, sensor.mAreaId);
return sensor.mAdapter.apply(value);
} catch (CarNotConnectedException ex) {
Log.e(TAG, "We got disconnected from Car Service", ex);
return null;
}
}
/**
* Returns a {@link LiveData} that tracks the fuel level in a range from 0 to 100.
*/
public LiveData<Integer> getFuelLevel() {
return Transformations.map(getSensor(Sensors.SENSOR_FUEL), (fuelValue) -> {
Float fuelCapacityValue = getSensorValue(Sensors.SENSOR_FUEL_CAPACITY);
if (fuelValue == null || fuelCapacityValue == null || fuelCapacityValue == 0) {
return null;
}
if (fuelValue < 0.0f) {
return 0;
}
if (fuelValue > fuelCapacityValue) {
return 100;
}
return Math.round(fuelValue / fuelCapacityValue * 100f);
});
}
/**
* Returns a {@link LiveData} that tracks the RPM x 1000
*/
public LiveData<String> getRPM() {
return Transformations.map(getSensor(Sensors.SENSOR_RPM), (rpmValue) -> {
return new DecimalFormat("#0.0").format(rpmValue / 1000f);
});
}
/**
* Returns a {@link LiveData} that tracks the speed in either mi/h or km/h depending on locale.
*/
public LiveData<Integer> getSpeed() {
return Transformations.map(getSensor(Sensors.SENSOR_SPEED), (speedValue) -> {
return Math.round(speedValue * mSpeedFactor);
});
}
/**
* Returns a {@link LiveData} that tracks the range the vehicle has until it runs out of gas.
*/
public LiveData<Integer> getRange() {
return Transformations.map(getSensor(Sensors.SENSOR_FUEL_RANGE), (rangeValue) -> {
return Math.round(rangeValue / mDistanceFactor);
});
}
/**
* Sets the activity selected to be displayed on the cluster when no driving directions are
* being provided.
*/
public void setFreeNavigationActivity(ComponentName activity) {
if (!Objects.equals(activity, mFreeNavigationActivity)) {
mFreeNavigationActivity = activity;
updateNavigationActivityLiveData();
}
}
/**
* Sets the activity currently being displayed on the cluster.
*/
public void setCurrentNavigationActivity(ComponentName activity) {
if (!Objects.equals(activity, mCurrentNavigationActivity)) {
mCurrentNavigationActivity = activity;
updateNavigationActivityLiveData();
}
}
/**
* Sets whether navigation focus is currently being granted or not.
*/
public void setNavigationFocus(boolean navigationFocus) {
if (mNavigationFocus.getValue() == null || mNavigationFocus.getValue() != navigationFocus) {
mNavigationFocus.setValue(navigationFocus);
updateNavigationActivityLiveData();
}
}
private void updateNavigationActivityLiveData() {
NavigationActivityState newState = calculateNavigationActivityState();
if (newState != mNavigationActivityStateLiveData.getValue()) {
mNavigationActivityStateLiveData.setValue(newState);
}
}
private NavigationActivityState calculateNavigationActivityState() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("Current state: current activity = '%s', free nav activity = "
+ "'%s', focus = %s", mCurrentNavigationActivity,
mFreeNavigationActivity,
mNavigationFocus.getValue()));
}
if (mNavigationFocus.getValue() != null && mNavigationFocus.getValue()) {
// Car service controls which activity is displayed while driving, so we assume this
// has already been taken care of.
return NavigationActivityState.VISIBLE;
} else if (mFreeNavigationActivity == null) {
return NavigationActivityState.NOT_SELECTED;
} else if (Objects.equals(mFreeNavigationActivity, mCurrentNavigationActivity)) {
return NavigationActivityState.VISIBLE;
} else {
return NavigationActivityState.LOADING;
}
}
@SuppressWarnings("unchecked")
private <T> MutableLiveData<T> getSensorMutableLiveData(Sensor<T> sensor) {
return (MutableLiveData<T>) mSensorLiveDatas
.computeIfAbsent(sensor, x -> new MutableLiveData<>());
}
}