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