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 
17 package com.android.car.setupwizardlib.util;
18 
19 import android.car.Car;
20 import android.car.CarNotConnectedException;
21 import android.car.VehicleAreaType;
22 import android.car.VehicleGear;
23 import android.car.VehiclePropertyIds;
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.car.drivingstate.CarUxRestrictionsManager;
26 import android.car.hardware.CarPropertyValue;
27 import android.car.hardware.property.CarPropertyManager;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.os.Handler;
33 import android.os.IBinder;
34 import android.os.Looper;
35 import android.util.Log;
36 
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 
40 /**
41  * Monitor that listens for changes in the driving state so that it can trigger an exit of the
42  * setup wizard when {@link CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP}
43  * is active.
44  */
45 public class CarDrivingStateMonitor implements
46         CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
47 
48     public static final String EXIT_BROADCAST_ACTION =
49             "com.android.car.setupwizardlib.driving_exit";
50 
51     public static final String INTENT_EXTRA_REASON = "reason";
52     public static final String REASON_GEAR_REVERSAL = "gear_reversal";
53 
54     private static final String TAG = "CarDrivingStateMonitor";
55     private static final long DISCONNECT_DELAY_MS = 700;
56 
57     private Car mCar;
58     private CarUxRestrictionsManager mRestrictionsManager;
59     private CarPropertyManager mCarPropertyManager;
60     // Need to track the number of times the monitor is started so a single stopMonitor call does
61     // not override them all.
62     private int mMonitorStartedCount;
63     // Flag that allows the monitor to be started for a ux restrictions check but not kept running.
64     // This is particularly useful when a DrivingExit is triggered by an app external to the base
65     // setup wizard package and we need to verify that it is a valid driving exit.
66     private boolean mStopMonitorAfterUxCheck;
67     private final Context mContext;
68     @VisibleForTesting
69     final Handler mHandler = new Handler(Looper.getMainLooper());
70     @VisibleForTesting
71     final Runnable mDisconnectRunnable = this::disconnectCarMonitor;
72 
73     private final CarPropertyManager.CarPropertyEventCallback mGearChangeCallback =
74             new CarPropertyManager.CarPropertyEventCallback() {
75         @SuppressWarnings("rawtypes")
76         @Override
77         public void onChangeEvent(CarPropertyValue value) {
78             switch (value.getPropertyId()) {
79                 case VehiclePropertyIds.GEAR_SELECTION:
80                     if ((Integer) value.getValue() == VehicleGear.GEAR_REVERSE) {
81                         Log.v(TAG, "Gear has reversed, exiting SetupWizard.");
82                         broadcastGearReversal();
83                     }
84                     break;
85             }
86         }
87 
88         @Override
89         public void onErrorEvent(int propertyId, int zone) {}
90     };
91 
CarDrivingStateMonitor(Context context)92     private CarDrivingStateMonitor(Context context) {
93         mContext = context.getApplicationContext();
94     }
95 
96     /**
97      * Returns the singleton instance of CarDrivingStateMonitor.
98      */
get(Context context)99     public static CarDrivingStateMonitor get(Context context) {
100         return CarHelperRegistry.getOrCreateWithAppContext(
101                 context.getApplicationContext(),
102                 CarDrivingStateMonitor.class,
103                 CarDrivingStateMonitor::new);
104     }
105 
106     /**
107      * Starts the monitor listening to driving state changes.
108      */
startMonitor()109     public synchronized void startMonitor() {
110         mMonitorStartedCount++;
111         if (mMonitorStartedCount == 0) {
112             Log.w(TAG, "MonitorStartedCount was negative");
113             return;
114         }
115         mHandler.removeCallbacks(mDisconnectRunnable);
116         Log.i(TAG, String.format(
117                 "Starting monitor, MonitorStartedCount = %d", mMonitorStartedCount));
118         if (mCar != null) {
119             if (mCar.isConnected()) {
120                 try {
121                     onUxRestrictionsChanged(mRestrictionsManager.getCurrentCarUxRestrictions());
122                 } catch (CarNotConnectedException e) {
123                     Log.e(TAG, "Car not connected", e);
124                 }
125             } else {
126                 try {
127                     mCar.connect();
128                 } catch (IllegalStateException e) {
129                     // Connection failure - already connected or connecting.
130                     Log.e(TAG, "Failure connecting to Car object.", e);
131                 }
132             }
133             return;
134         }
135         mCar = Car.createCar(mContext, new ServiceConnection() {
136             @Override
137             public void onServiceConnected(ComponentName name, IBinder service) {
138                 try {
139                     registerPropertyManager();
140                     registerRestrictionsManager();
141 
142                 } catch (CarNotConnectedException e) {
143                     Log.e(TAG, "Car not connected", e);
144                 }
145             }
146 
147             @Override
148             public void onServiceDisconnected(ComponentName name) {
149                 try {
150                     if (mRestrictionsManager != null) {
151                         mRestrictionsManager.unregisterListener();
152                         mRestrictionsManager = null;
153                     }
154                 } catch (CarNotConnectedException e) {
155                     Log.e(TAG, "Car not connected", e);
156                 }
157             }
158         });
159         try {
160             mCar.connect();
161         } catch (IllegalStateException e) {
162             // Connection failure - already connected or connecting.
163             Log.e(TAG, "Failure connecting to Car object.", e);
164         }
165     }
166 
167     /**
168      * Stops the monitor from listening for driving state changes. This will only occur after a
169      * set delay so that calling stop/start in quick succession doesn't actually need to reconnect
170      * to the service repeatedly. This monitor also maintains parity between started and stopped so
171      * 2 started calls requires two stop calls to stop.
172      */
stopMonitor()173     public synchronized void stopMonitor() {
174         if (isVerboseLoggable()) {
175             Log.v(TAG, "stopMonitor");
176         }
177         mHandler.removeCallbacks(mDisconnectRunnable);
178         mMonitorStartedCount--;
179         if (mMonitorStartedCount == 0) {
180             if (isVerboseLoggable()) {
181                 Log.v(TAG, "Scheduling driving monitor timeout");
182             }
183             mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY_MS);
184         }
185         if (mMonitorStartedCount < 0) {
186             mMonitorStartedCount = 0;
187         }
188     }
189 
disconnectCarMonitor()190     private void disconnectCarMonitor() {
191         if (mMonitorStartedCount > 0) {
192             if (isVerboseLoggable()) {
193                 Log.v(TAG, "MonitorStartedCount > 0, do nothing");
194             }
195             return;
196         }
197         Log.i(TAG, "Disconnecting Car Monitor");
198         try {
199             if (mRestrictionsManager != null) {
200                 mRestrictionsManager.unregisterListener();
201                 mRestrictionsManager = null;
202             }
203             if (mCarPropertyManager != null) {
204                 mCarPropertyManager.unregisterCallback(mGearChangeCallback);
205                 mCarPropertyManager = null;
206             }
207         } catch (CarNotConnectedException e) {
208             Log.e(TAG, "Car not connected for unregistering listener", e);
209         }
210 
211         if (mCar == null || !mCar.isConnected()) {
212             return;
213         }
214 
215         try {
216             mCar.disconnect();
217         } catch (IllegalStateException e) {
218             // Connection failure - already disconnected or disconnecting.
219             Log.e(TAG, "Failure disconnecting from Car object", e);
220         }
221     }
222 
223     /**
224      * Returns {@code true} if the current driving state restricts setup from being completed.
225      */
checkIsSetupRestricted()226     public boolean checkIsSetupRestricted() {
227         if (mMonitorStartedCount <= 0 && (mCar == null || !mCar.isConnected())) {
228             if (isVerboseLoggable()) {
229                 Log.v(TAG, "Starting monitor to perform restriction check, returning false for "
230                         + "restrictions in the meantime");
231             }
232             mStopMonitorAfterUxCheck = true;
233             startMonitor();
234             return false;
235         }
236         if (mRestrictionsManager == null) {
237             if (isVerboseLoggable()) {
238                 Log.v(TAG, "Restrictions manager null in checkIsSetupRestricted, returning false");
239             }
240             return false;
241         }
242         try {
243             return checkIsSetupRestricted(mRestrictionsManager.getCurrentCarUxRestrictions());
244         } catch (CarNotConnectedException e) {
245             Log.e(TAG, "CarNotConnected in checkIsSetupRestricted, returning false", e);
246         }
247         return false;
248     }
249 
checkIsSetupRestricted(@ullable CarUxRestrictions restrictionInfo)250     private boolean checkIsSetupRestricted(@Nullable CarUxRestrictions restrictionInfo) {
251         if (restrictionInfo == null) {
252             if (isVerboseLoggable()) {
253                 Log.v(TAG, "checkIsSetupRestricted restrictionInfo is null, returning false");
254             }
255             return false;
256         }
257         int activeRestrictions = restrictionInfo.getActiveRestrictions();
258         if (isVerboseLoggable()) {
259             Log.v(TAG, "activeRestrictions are " + activeRestrictions);
260         }
261         // There must be at least some restriction in place.
262         return activeRestrictions != 0;
263     }
264 
265     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictionInfo)266     public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
267         // Check if setup restriction is active.
268         if (isVerboseLoggable()) {
269             Log.v(TAG, "onUxRestrictionsChanged");
270         }
271 
272         // Get the current CarUxRestrictions rather than trusting the ones passed in.
273         // This prevents in part interference from other applications triggering a setup wizard
274         // exit unnecessarily, though the broadcast is also checked on the receiver side.
275         if (mRestrictionsManager != null) {
276             try {
277                 restrictionInfo = mRestrictionsManager.getCurrentCarUxRestrictions();
278             } catch (CarNotConnectedException e) {
279                 Log.e(TAG, "Car not connected in onUxRestrictionsChanged, doing nothing.", e);
280             }
281         }
282 
283         if (checkIsSetupRestricted(restrictionInfo)) {
284             if (isVerboseLoggable()) {
285                 Log.v(TAG, "Triggering driving exit broadcast");
286             }
287             Intent broadcastIntent = new Intent();
288             broadcastIntent.setAction(EXIT_BROADCAST_ACTION);
289             mContext.sendBroadcast(broadcastIntent);
290         }
291     }
292 
isVerboseLoggable()293     private boolean isVerboseLoggable() {
294         return Log.isLoggable(TAG, Log.VERBOSE);
295     }
296 
297     /**
298      * Resets the car driving state monitor. This is only for use in testing.
299      */
300     @VisibleForTesting
reset(Context context)301     public static void reset(Context context) {
302         CarHelperRegistry.getRegistry(context).putHelper(
303                 CarDrivingStateMonitor.class, new CarDrivingStateMonitor(context));
304     }
305 
306     /** Testing only */
307     @VisibleForTesting
replace(Context context, CarDrivingStateMonitor monitor)308     public static void replace(Context context, CarDrivingStateMonitor monitor) {
309         CarHelperRegistry.getRegistry(context).putHelper(CarDrivingStateMonitor.class, monitor);
310     }
311 
registerRestrictionsManager()312     private void registerRestrictionsManager() {
313         mRestrictionsManager = (CarUxRestrictionsManager)
314                 mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
315         if (mRestrictionsManager == null) {
316             Log.e(TAG, "Unable to get CarUxRestrictionsManager");
317             return;
318         }
319         onUxRestrictionsChanged(mRestrictionsManager.getCurrentCarUxRestrictions());
320         mRestrictionsManager.registerListener(CarDrivingStateMonitor.this);
321         if (mStopMonitorAfterUxCheck) {
322             mStopMonitorAfterUxCheck = false;
323             stopMonitor();
324         }
325     }
326 
registerPropertyManager()327     private void registerPropertyManager() {
328         mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
329         if (mCarPropertyManager == null) {
330             Log.e(TAG, "Unable to get CarPropertyManager");
331             return;
332         }
333         mCarPropertyManager.registerCallback(
334                 mGearChangeCallback, VehiclePropertyIds.GEAR_SELECTION,
335                 CarPropertyManager.SENSOR_RATE_ONCHANGE);
336         CarPropertyValue<Integer> gearSelection =
337                 mCarPropertyManager.getProperty(Integer.class, VehiclePropertyIds.GEAR_SELECTION,
338                     VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL);
339         if (gearSelection != null
340                 && gearSelection.getStatus() == CarPropertyValue.STATUS_AVAILABLE) {
341             if (gearSelection.getValue() == VehicleGear.GEAR_REVERSE) {
342                 Log.v(TAG, "SetupWizard started when gear is in reverse, exiting.");
343                 broadcastGearReversal();
344             }
345         } else {
346             Log.e(TAG, "GEAR_SELECTION is not available.");
347         }
348     }
349 
broadcastGearReversal()350     private void broadcastGearReversal() {
351         Intent intent = new Intent();
352         intent.setAction(EXIT_BROADCAST_ACTION);
353         intent.putExtra(INTENT_EXTRA_REASON, REASON_GEAR_REVERSAL);
354         mContext.sendBroadcast(intent);
355     }
356 }
357