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