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