/* * 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 static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; 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.VehiclePropertyIds; 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 float PROPERTIES_REFRESH_RATE_UI = 5f; 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 mNavigationActivityStateLiveData = new MutableLiveData<>(); private final MutableLiveData mNavigationFocus = new MutableLiveData<>(false); private Car mCar; private CarAppFocusManager mCarAppFocusManager; private CarPropertyManager mCarPropertyManager; private Map, 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.subscribePropertyEvents(propertyId, PROPERTIES_REFRESH_RATE_UI, mCarPropertyEventCallback); } 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 == VEHICLE_AREA_TYPE_GLOBAL || (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 void setSensorValue(Sensor 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 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 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 data type of such sensor */ @SuppressWarnings("unchecked") @NonNull public LiveData getSensor(@NonNull Sensor sensor) { return getSensorMutableLiveData(Preconditions.checkNotNull(sensor)); } /** * Returns the current value of the sensor, directly from the VHAL. * * @param sensor sensor to read * @param data type of such sensor */ @Nullable public T getSensorValue(@NonNull Sensor sensor) { if (mCarPropertyManager == null) { Log.e(TAG, "CarPropertyManager reference is null, car service is disconnected."); return null; } CarPropertyValue carPropertyValue = mCarPropertyManager.getProperty(sensor.mPropertyId, sensor.mAreaId); if (carPropertyValue == null) { Log.w(TAG, "Property ID: " + VehiclePropertyIds.toString(sensor.mPropertyId) + " Area ID: 0x" + Integer.toHexString(sensor.mAreaId) + " returned null from CarPropertyManager#getProperty()"); return null; } return sensor.mAdapter.apply(carPropertyValue); } /** * Returns a {@link LiveData} that tracks the fuel level in a range from 0 to 100. */ public LiveData 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 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 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 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 MutableLiveData getSensorMutableLiveData(Sensor sensor) { return (MutableLiveData) mSensorLiveDatas .computeIfAbsent(sensor, x -> new MutableLiveData<>()); } }