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