1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.car.cluster; 17 18 import android.annotation.Nullable; 19 import android.app.Application; 20 import android.car.Car; 21 import android.car.CarAppFocusManager; 22 import android.car.CarNotConnectedException; 23 import android.car.VehicleAreaType; 24 import android.car.cluster.sensors.Sensor; 25 import android.car.cluster.sensors.Sensors; 26 import android.car.hardware.CarPropertyValue; 27 import android.car.hardware.property.CarPropertyManager; 28 import android.content.ComponentName; 29 import android.content.ServiceConnection; 30 import android.os.IBinder; 31 import android.util.Log; 32 import android.util.TypedValue; 33 34 import androidx.annotation.NonNull; 35 import androidx.core.util.Preconditions; 36 import androidx.lifecycle.AndroidViewModel; 37 import androidx.lifecycle.LiveData; 38 import androidx.lifecycle.MutableLiveData; 39 import androidx.lifecycle.Transformations; 40 41 import java.text.DecimalFormat; 42 import java.util.HashMap; 43 import java.util.Map; 44 import java.util.Objects; 45 46 /** 47 * {@link AndroidViewModel} for cluster information. 48 */ 49 public class ClusterViewModel extends AndroidViewModel { 50 private static final String TAG = "Cluster.ViewModel"; 51 52 private static final int PROPERTIES_REFRESH_RATE_UI = 5; 53 54 private float mSpeedFactor; 55 private float mDistanceFactor; 56 57 public enum NavigationActivityState { 58 /** No activity has been selected to be displayed on the navigation fragment yet */ 59 NOT_SELECTED, 60 /** An activity has been selected, but it is not yet visible to the user */ 61 LOADING, 62 /** Navigation activity is visible to the user */ 63 VISIBLE, 64 } 65 66 private ComponentName mFreeNavigationActivity; 67 private ComponentName mCurrentNavigationActivity; 68 private final MutableLiveData<NavigationActivityState> mNavigationActivityStateLiveData = 69 new MutableLiveData<>(); 70 private final MutableLiveData<Boolean> mNavigationFocus = new MutableLiveData<>(false); 71 private Car mCar; 72 private CarAppFocusManager mCarAppFocusManager; 73 private CarPropertyManager mCarPropertyManager; 74 private Map<Sensor<?>, MutableLiveData<?>> mSensorLiveDatas = new HashMap<>(); 75 76 private ServiceConnection mCarServiceConnection = new ServiceConnection() { 77 @Override 78 public void onServiceConnected(ComponentName name, IBinder service) { 79 try { 80 Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service); 81 82 registerAppFocusListener(); 83 registerCarPropertiesListener(); 84 } catch (CarNotConnectedException e) { 85 Log.e(TAG, "onServiceConnected: error obtaining manager", e); 86 } 87 } 88 89 @Override 90 public void onServiceDisconnected(ComponentName name) { 91 Log.i(TAG, "onServiceDisconnected, name: " + name); 92 mCarAppFocusManager = null; 93 mCarPropertyManager = null; 94 } 95 }; 96 registerAppFocusListener()97 private void registerAppFocusListener() throws CarNotConnectedException { 98 mCarAppFocusManager = (CarAppFocusManager) mCar.getCarManager( 99 Car.APP_FOCUS_SERVICE); 100 if (mCarAppFocusManager != null) { 101 mCarAppFocusManager.addFocusListener( 102 (appType, active) -> setNavigationFocus(active), 103 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION); 104 } else { 105 Log.e(TAG, "onServiceConnected: unable to obtain CarAppFocusManager"); 106 } 107 } 108 registerCarPropertiesListener()109 private void registerCarPropertiesListener() throws CarNotConnectedException { 110 Sensors sensors = Sensors.getInstance(); 111 mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE); 112 for (Integer propertyId : sensors.getPropertyIds()) { 113 try { 114 mCarPropertyManager.registerCallback(mCarPropertyEventCallback, 115 propertyId, PROPERTIES_REFRESH_RATE_UI); 116 } catch (SecurityException ex) { 117 Log.e(TAG, "onServiceConnected: Unable to listen to car property: " + propertyId 118 + " sensors: " + sensors.getSensorsForPropertyId(propertyId), ex); 119 } 120 } 121 } 122 123 private CarPropertyManager.CarPropertyEventCallback mCarPropertyEventCallback = 124 new CarPropertyManager.CarPropertyEventCallback() { 125 @Override 126 public void onChangeEvent(CarPropertyValue value) { 127 if (Log.isLoggable(TAG, Log.DEBUG)) { 128 Log.d(TAG, 129 "CarProperty change: property " + value.getPropertyId() + ", area" 130 + value.getAreaId() + ", value: " + value.getValue()); 131 } 132 for (Sensor<?> sensorId : Sensors.getInstance() 133 .getSensorsForPropertyId(value.getPropertyId())) { 134 if (sensorId.mAreaId == Sensors.GLOBAL_AREA_ID 135 || (sensorId.mAreaId & value.getAreaId()) != 0) { 136 setSensorValue(sensorId, value); 137 } 138 } 139 } 140 141 @Override 142 public void onErrorEvent(int propId, int zone) { 143 for (Sensor<?> sensorId : Sensors.getInstance().getSensorsForPropertyId( 144 propId)) { 145 if (sensorId.mAreaId == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL 146 || (sensorId.mAreaId & zone) != 0) { 147 setSensorValue(sensorId, null); 148 } 149 } 150 } 151 152 private <T> void setSensorValue(Sensor<T> id, CarPropertyValue<?> value) { 153 T newValue = value != null ? id.mAdapter.apply(value) : null; 154 if (Log.isLoggable(TAG, Log.DEBUG)) { 155 Log.d(TAG, "Sensor " + id.mName + " = " + newValue); 156 } 157 getSensorMutableLiveData(id).setValue(newValue); 158 } 159 }; 160 161 /** 162 * New {@link ClusterViewModel} instance 163 */ ClusterViewModel(@onNull Application application)164 public ClusterViewModel(@NonNull Application application) { 165 super(application); 166 mCar = Car.createCar(application, mCarServiceConnection); 167 mCar.connect(); 168 169 TypedValue tv = new TypedValue(); 170 getApplication().getResources().getValue(R.dimen.speed_factor, tv, true); 171 mSpeedFactor = tv.getFloat(); 172 173 getApplication().getResources().getValue(R.dimen.distance_factor, tv, true); 174 mDistanceFactor = tv.getFloat(); 175 } 176 177 @Override onCleared()178 protected void onCleared() { 179 super.onCleared(); 180 mCar.disconnect(); 181 mCar = null; 182 mCarAppFocusManager = null; 183 mCarPropertyManager = null; 184 } 185 186 /** 187 * Returns a {@link LiveData} providing the current state of the activity displayed on the 188 * navigation fragment. 189 */ getNavigationActivityState()190 public LiveData<NavigationActivityState> getNavigationActivityState() { 191 return mNavigationActivityStateLiveData; 192 } 193 194 /** 195 * Returns a {@link LiveData} indicating whether navigation focus is currently being granted 196 * or not. This indicates whether a navigation application is currently providing driving 197 * directions. 198 */ getNavigationFocus()199 public LiveData<Boolean> getNavigationFocus() { 200 return mNavigationFocus; 201 } 202 203 /** 204 * Returns a {@link LiveData} that tracks the value of a given car sensor. Each sensor has its 205 * own data type. The list of all supported sensors can be found at {@link Sensors} 206 * 207 * @param sensor sensor to observe 208 * @param <T> data type of such sensor 209 */ 210 @SuppressWarnings("unchecked") 211 @NonNull getSensor(@onNull Sensor<T> sensor)212 public <T> LiveData<T> getSensor(@NonNull Sensor<T> sensor) { 213 return getSensorMutableLiveData(Preconditions.checkNotNull(sensor)); 214 } 215 216 /** 217 * Returns the current value of the sensor, directly from the VHAL. 218 * 219 * @param sensor sensor to read 220 * @param <V> VHAL data type 221 * @param <T> data type of such sensor 222 */ 223 @Nullable getSensorValue(@onNull Sensor<T> sensor)224 public <T> T getSensorValue(@NonNull Sensor<T> sensor) { 225 try { 226 CarPropertyValue<?> value = mCarPropertyManager 227 .getProperty(sensor.mPropertyId, sensor.mAreaId); 228 return sensor.mAdapter.apply(value); 229 } catch (CarNotConnectedException ex) { 230 Log.e(TAG, "We got disconnected from Car Service", ex); 231 return null; 232 } 233 } 234 235 /** 236 * Returns a {@link LiveData} that tracks the fuel level in a range from 0 to 100. 237 */ getFuelLevel()238 public LiveData<Integer> getFuelLevel() { 239 return Transformations.map(getSensor(Sensors.SENSOR_FUEL), (fuelValue) -> { 240 Float fuelCapacityValue = getSensorValue(Sensors.SENSOR_FUEL_CAPACITY); 241 if (fuelValue == null || fuelCapacityValue == null || fuelCapacityValue == 0) { 242 return null; 243 } 244 if (fuelValue < 0.0f) { 245 return 0; 246 } 247 if (fuelValue > fuelCapacityValue) { 248 return 100; 249 } 250 return Math.round(fuelValue / fuelCapacityValue * 100f); 251 }); 252 } 253 254 /** 255 * Returns a {@link LiveData} that tracks the RPM x 1000 256 */ getRPM()257 public LiveData<String> getRPM() { 258 return Transformations.map(getSensor(Sensors.SENSOR_RPM), (rpmValue) -> { 259 return new DecimalFormat("#0.0").format(rpmValue / 1000f); 260 }); 261 } 262 263 /** 264 * Returns a {@link LiveData} that tracks the speed in either mi/h or km/h depending on locale. 265 */ 266 public LiveData<Integer> getSpeed() { 267 return Transformations.map(getSensor(Sensors.SENSOR_SPEED), (speedValue) -> { 268 return Math.round(speedValue * mSpeedFactor); 269 }); 270 } 271 272 /** 273 * Returns a {@link LiveData} that tracks the range the vehicle has until it runs out of gas. 274 */ 275 public LiveData<Integer> getRange() { 276 return Transformations.map(getSensor(Sensors.SENSOR_FUEL_RANGE), (rangeValue) -> { 277 return Math.round(rangeValue / mDistanceFactor); 278 }); 279 } 280 281 /** 282 * Sets the activity selected to be displayed on the cluster when no driving directions are 283 * being provided. 284 */ 285 public void setFreeNavigationActivity(ComponentName activity) { 286 if (!Objects.equals(activity, mFreeNavigationActivity)) { 287 mFreeNavigationActivity = activity; 288 updateNavigationActivityLiveData(); 289 } 290 } 291 292 /** 293 * Sets the activity currently being displayed on the cluster. 294 */ 295 public void setCurrentNavigationActivity(ComponentName activity) { 296 if (!Objects.equals(activity, mCurrentNavigationActivity)) { 297 mCurrentNavigationActivity = activity; 298 updateNavigationActivityLiveData(); 299 } 300 } 301 302 /** 303 * Sets whether navigation focus is currently being granted or not. 304 */ 305 public void setNavigationFocus(boolean navigationFocus) { 306 if (mNavigationFocus.getValue() == null || mNavigationFocus.getValue() != navigationFocus) { 307 mNavigationFocus.setValue(navigationFocus); 308 updateNavigationActivityLiveData(); 309 } 310 } 311 312 private void updateNavigationActivityLiveData() { 313 NavigationActivityState newState = calculateNavigationActivityState(); 314 if (newState != mNavigationActivityStateLiveData.getValue()) { 315 mNavigationActivityStateLiveData.setValue(newState); 316 } 317 } 318 319 private NavigationActivityState calculateNavigationActivityState() { 320 if (Log.isLoggable(TAG, Log.DEBUG)) { 321 Log.d(TAG, String.format("Current state: current activity = '%s', free nav activity = " 322 + "'%s', focus = %s", mCurrentNavigationActivity, 323 mFreeNavigationActivity, 324 mNavigationFocus.getValue())); 325 } 326 if (mNavigationFocus.getValue() != null && mNavigationFocus.getValue()) { 327 // Car service controls which activity is displayed while driving, so we assume this 328 // has already been taken care of. 329 return NavigationActivityState.VISIBLE; 330 } else if (mFreeNavigationActivity == null) { 331 return NavigationActivityState.NOT_SELECTED; 332 } else if (Objects.equals(mFreeNavigationActivity, mCurrentNavigationActivity)) { 333 return NavigationActivityState.VISIBLE; 334 } else { 335 return NavigationActivityState.LOADING; 336 } 337 } 338 339 @SuppressWarnings("unchecked") 340 private <T> MutableLiveData<T> getSensorMutableLiveData(Sensor<T> sensor) { 341 return (MutableLiveData<T>) mSensorLiveDatas 342 .computeIfAbsent(sensor, x -> new MutableLiveData<>()); 343 } 344 } 345