blob: 891e01a4eeccd092cdc7a5c824e40c201cc62ff5 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.setupwizardlib.util;
import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.VehicleAreaType;
import android.car.VehicleGear;
import android.car.VehiclePropertyIds;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
/**
* Monitor that listens for changes in the driving state so that it can trigger an exit of the
* setup wizard when {@link CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP}
* is active.
*/
public class CarDrivingStateMonitor implements
CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
public static final String EXIT_BROADCAST_ACTION =
"com.android.car.setupwizardlib.driving_exit";
private static final String TAG = "CarDrivingStateMonitor";
private static final long DISCONNECT_DELAY_MS = 700;
private static final String SETUP_PACKAGE = "com.google.android.car.setupwizard";
private static final String SETUP_CLASS = SETUP_PACKAGE + ".ExitActivity";
private Car mCar;
private CarUxRestrictionsManager mRestrictionsManager;
private CarPropertyManager mCarPropertyManager;
// Need to track the number of times the monitor is started so a single stopMonitor call does
// not override them all.
private int mMonitorStartedCount;
// Flag that allows the monitor to be started for a ux restrictions check but not kept running.
// This is particularly useful when a DrivingExit is triggered by an app external to the base
// setup wizard package and we need to verify that it is a valid driving exit.
private boolean mStopMonitorAfterUxCheck;
private final Context mContext;
@VisibleForTesting
final Handler mHandler = new Handler(Looper.getMainLooper());
@VisibleForTesting
final Runnable mDisconnectRunnable = this::disconnectCarMonitor;
private final CarPropertyManager.CarPropertyEventCallback mGearChangeCallback =
new CarPropertyManager.CarPropertyEventCallback() {
@SuppressWarnings("rawtypes")
@Override
public void onChangeEvent(CarPropertyValue value) {
switch (value.getPropertyId()) {
case VehiclePropertyIds.GEAR_SELECTION:
if ((Integer) value.getValue() == VehicleGear.GEAR_REVERSE) {
Log.v(TAG, "Gear has reversed, exiting SetupWizard.");
sendExitActivityIntent();
}
break;
}
}
@Override
public void onErrorEvent(int propertyId, int zone) {}
};
private CarDrivingStateMonitor(Context context) {
mContext = context.getApplicationContext();
}
/**
* Returns the singleton instance of CarDrivingStateMonitor.
*/
public static CarDrivingStateMonitor get(Context context) {
return CarHelperRegistry.getOrCreateWithAppContext(
context.getApplicationContext(),
CarDrivingStateMonitor.class,
CarDrivingStateMonitor::new);
}
/**
* Starts the monitor listening to driving state changes.
*/
public synchronized void startMonitor() {
if (isVerboseLoggable()) {
Log.v(TAG, "Starting monitor");
}
mMonitorStartedCount++;
if (mMonitorStartedCount == 0) {
return;
}
mHandler.removeCallbacks(mDisconnectRunnable);
if (mCar != null) {
if (mCar.isConnected()) {
try {
onUxRestrictionsChanged(mRestrictionsManager.getCurrentCarUxRestrictions());
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected", e);
}
} else {
try {
mCar.connect();
} catch (IllegalStateException e) {
// Connection failure - already connected or connecting.
Log.e(TAG, "Failure connecting to Car object.", e);
}
}
return;
}
mCar = Car.createCar(mContext, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
registerPropertyManager();
registerRestrictionsManager();
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
try {
if (mRestrictionsManager != null) {
mRestrictionsManager.unregisterListener();
mRestrictionsManager = null;
}
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected", e);
}
}
});
try {
mCar.connect();
} catch (IllegalStateException e) {
// Connection failure - already connected or connecting.
Log.e(TAG, "Failure connecting to Car object.", e);
}
}
/**
* Stops the monitor from listening for driving state changes. This will only occur after a
* set delay so that calling stop/start in quick succession doesn't actually need to reconnect
* to the service repeatedly. This monitor also maintains parity between started and stopped so
* 2 started calls requires two stop calls to stop.
*/
public synchronized void stopMonitor() {
if (isVerboseLoggable()) {
Log.v(TAG, "stopMonitor");
}
mHandler.removeCallbacks(mDisconnectRunnable);
mMonitorStartedCount--;
if (mMonitorStartedCount == 0) {
if (isVerboseLoggable()) {
Log.v(TAG, "Scheduling driving monitor timeout");
}
mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY_MS);
}
if (mMonitorStartedCount < 0) {
mMonitorStartedCount = 0;
}
}
private void disconnectCarMonitor() {
if (isVerboseLoggable()) {
Log.v(TAG, "Timeout finished, disconnecting Car Monitor");
}
if (mMonitorStartedCount > 0) {
return;
}
try {
if (mRestrictionsManager != null) {
mRestrictionsManager.unregisterListener();
mRestrictionsManager = null;
}
if (mCarPropertyManager != null) {
mCarPropertyManager.unregisterCallback(mGearChangeCallback);
mCarPropertyManager = null;
}
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected for unregistering listener", e);
}
if (mCar == null || !mCar.isConnected()) {
return;
}
try {
mCar.disconnect();
} catch (IllegalStateException e) {
// Connection failure - already disconnected or disconnecting.
Log.e(TAG, "Failure disconnecting from Car object", e);
}
}
/**
* Returns {@code true} if the current driving state restricts setup from being completed.
*/
public boolean checkIsSetupRestricted() {
if (mMonitorStartedCount <= 0 && (mCar == null || !mCar.isConnected())) {
if (isVerboseLoggable()) {
Log.v(TAG, "Starting monitor to perform restriction check, returning false for "
+ "restrictions in the meantime");
}
mStopMonitorAfterUxCheck = true;
startMonitor();
return false;
}
if (mRestrictionsManager == null) {
if (isVerboseLoggable()) {
Log.v(TAG, "Restrictions manager null in checkIsSetupRestricted, returning false");
}
return false;
}
try {
return checkIsSetupRestricted(mRestrictionsManager.getCurrentCarUxRestrictions());
} catch (CarNotConnectedException e) {
Log.e(TAG, "CarNotConnected in checkIsSetupRestricted, returning false", e);
}
return false;
}
private boolean checkIsSetupRestricted(CarUxRestrictions restrictionInfo) {
return (restrictionInfo.getActiveRestrictions()
& CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP) != 0;
}
@Override
public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
// Check if setup restriction is active.
if (isVerboseLoggable()) {
Log.v(TAG, "onUxRestrictionsChanged");
}
// Get the current CarUxRestrictions rather than trusting the ones passed in.
// This prevents in part interference from other applications triggering a setup wizard
// exit unnecessarily, though the broadcast is also checked on the receiver side.
if (mRestrictionsManager != null) {
try {
restrictionInfo = mRestrictionsManager.getCurrentCarUxRestrictions();
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected in onUxRestrictionsChanged, doing nothing.", e);
}
}
if (checkIsSetupRestricted(restrictionInfo)) {
if (isVerboseLoggable()) {
Log.v(TAG, "Triggering driving exit broadcast");
}
Intent broadcastIntent = new Intent();
broadcastIntent.setAction(EXIT_BROADCAST_ACTION);
mContext.sendBroadcast(broadcastIntent);
}
}
private boolean isVerboseLoggable() {
return Log.isLoggable(TAG, Log.VERBOSE);
}
/**
* Resets the car driving state monitor. This is only for use in testing.
*/
@VisibleForTesting
public static void reset(Context context) {
CarHelperRegistry.getRegistry(context).putHelper(
CarDrivingStateMonitor.class, new CarDrivingStateMonitor(context));
}
private void registerRestrictionsManager() {
mRestrictionsManager = (CarUxRestrictionsManager)
mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
if (mRestrictionsManager == null) {
Log.e(TAG, "Unable to get CarUxRestrictionsManager");
return;
}
onUxRestrictionsChanged(mRestrictionsManager.getCurrentCarUxRestrictions());
mRestrictionsManager.registerListener(CarDrivingStateMonitor.this);
if (mStopMonitorAfterUxCheck) {
mStopMonitorAfterUxCheck = false;
stopMonitor();
}
}
private void registerPropertyManager() {
mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
if (mCarPropertyManager == null) {
Log.e(TAG, "Unable to get CarPropertyManager");
return;
}
mCarPropertyManager.registerCallback(
mGearChangeCallback, VehiclePropertyIds.GEAR_SELECTION,
CarPropertyManager.SENSOR_RATE_ONCHANGE);
CarPropertyValue<Integer> gearSelection =
mCarPropertyManager.getProperty(Integer.class, VehiclePropertyIds.GEAR_SELECTION,
VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL);
if (gearSelection != null
&& gearSelection.getStatus() == CarPropertyValue.STATUS_AVAILABLE) {
if (gearSelection.getValue() == VehicleGear.GEAR_REVERSE) {
Log.v(TAG, "SetupWizard started when gear is in reverse, exiting.");
sendExitActivityIntent();
}
} else {
Log.e(TAG, "GEAR_SELECTION is not available.");
}
}
private void sendExitActivityIntent() {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(new ComponentName(SETUP_PACKAGE, SETUP_CLASS));
mContext.startActivity(intent);
}
}