/* * Copyright (c) 2016, 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.hvac; import android.app.Service; import android.car.Car; import android.car.CarNotConnectedException; import android.car.VehicleAreaSeat; import android.car.VehicleAreaWindow; import android.car.hardware.CarPropertyConfig; import android.car.hardware.CarPropertyValue; import android.car.hardware.hvac.CarHvacManager; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.util.Log; import java.util.ArrayList; import java.util.List; import javax.annotation.concurrent.GuardedBy; public class HvacController extends Service { private static final String TAG = "HvacController"; private static final int DRIVER_ZONE_ID = VehicleAreaSeat.SEAT_ROW_1_LEFT | VehicleAreaSeat.SEAT_ROW_2_LEFT | VehicleAreaSeat.SEAT_ROW_2_CENTER; private static final int PASSENGER_ZONE_ID = VehicleAreaSeat.SEAT_ROW_1_RIGHT | VehicleAreaSeat.SEAT_ROW_2_RIGHT; public static final int[] AIRFLOW_STATES = new int[]{ CarHvacManager.FAN_DIRECTION_FACE, CarHvacManager.FAN_DIRECTION_FLOOR, (CarHvacManager.FAN_DIRECTION_FACE | CarHvacManager.FAN_DIRECTION_FLOOR) }; // Hardware specific value for the front seats public static final int SEAT_ALL = DRIVER_ZONE_ID | PASSENGER_ZONE_ID; /** * Callback for receiving updates from the hvac manager. A Callback can be * registered using {@link #registerCallback}. */ public static abstract class Callback { public void onPassengerTemperatureChange(CarPropertyValue propValue) { } public void onDriverTemperatureChange(CarPropertyValue propValue) { } public void onFanSpeedChange(int position) { } public void onAcStateChange(boolean isOn) { } public void onFrontDefrosterChange(boolean isOn) { } public void onRearDefrosterChange(boolean isOn) { } public void onPassengerSeatWarmerChange(int level) { } public void onDriverSeatWarmerChange(int level) { } public void onFanDirectionChange(int direction) { } public void onAirCirculationChange(boolean isOn) { } public void onAutoModeChange(boolean isOn) { } public void onHvacPowerChange(boolean isOn) { } } public class LocalBinder extends Binder { HvacController getService() { return HvacController.this; } } private final Binder mBinder = new LocalBinder(); private Car mCarApiClient; private CarHvacManager mHvacManager; private Object mHvacManagerReady = new Object(); private HvacPolicy mPolicy; @GuardedBy("mCallbacks") private List mCallbacks = new ArrayList<>(); private DataStore mDataStore = new DataStore(); @Override public void onCreate() { super.onCreate(); if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { mCarApiClient = Car.createCar(this, mCarServiceConnection); mCarApiClient.connect(); } } @Override public void onDestroy() { super.onDestroy(); if (mHvacManager != null) { mHvacManager.unregisterCallback(mHardwareCallback); } if (mCarApiClient != null) { mCarApiClient.disconnect(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } @Override public IBinder onBind(Intent intent) { return mBinder; } public void registerCallback(Callback callback) { synchronized (mCallbacks) { mCallbacks.add(callback); } } public void unregisterCallback(Callback callback) { synchronized (mCallbacks) { mCallbacks.remove(callback); } } private void initHvacManager(CarHvacManager carHvacManager) { mHvacManager = carHvacManager; List properties = null; try { properties = mHvacManager.getPropertyList(); mPolicy = new HvacPolicy(HvacController.this, properties); mHvacManager.registerCallback(mHardwareCallback); } catch (CarNotConnectedException e) { Log.e(TAG, "Car not connected in HVAC"); } } private final ServiceConnection mCarServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mHvacManagerReady) { try { initHvacManager((CarHvacManager) mCarApiClient.getCarManager( Car.HVAC_SERVICE)); mHvacManagerReady.notifyAll(); } catch (CarNotConnectedException e) { Log.e(TAG, "Car not connected in onServiceConnected"); } } } @Override public void onServiceDisconnected(ComponentName name) { } }; private final CarHvacManager.CarHvacEventCallback mHardwareCallback = new CarHvacManager.CarHvacEventCallback() { @Override public void onChangeEvent(final CarPropertyValue val) { int areaId = val.getAreaId(); switch (val.getPropertyId()) { case CarHvacManager.ID_ZONED_AC_ON: handleAcStateUpdate(getValue(val)); break; case CarHvacManager.ID_ZONED_FAN_DIRECTION: handleFanPositionUpdate(areaId, getValue(val)); break; case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT: handleFanSpeedUpdate(areaId, getValue(val)); break; case CarHvacManager.ID_ZONED_TEMP_SETPOINT: handleTempUpdate(val); break; case CarHvacManager.ID_WINDOW_DEFROSTER_ON: handleDefrosterUpdate(areaId, getValue(val)); break; case CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON: handleAirCirculationUpdate(getValue(val)); break; case CarHvacManager.ID_ZONED_SEAT_TEMP: handleSeatWarmerUpdate(areaId, getValue(val)); break; case CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON: handleAutoModeUpdate(getValue(val)); break; case CarHvacManager.ID_ZONED_HVAC_POWER_ON: handleHvacPowerOn(getValue(val)); break; default: if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unhandled HVAC event, id: " + val.getPropertyId()); } } } @Override public void onErrorEvent(final int propertyId, final int zone) { } }; @SuppressWarnings("unchecked") public static E getValue(CarPropertyValue propertyValue) { return (E) propertyValue.getValue(); } public static boolean isAvailable(CarPropertyValue propertyValue) { return propertyValue.getStatus() == CarPropertyValue.STATUS_AVAILABLE; } void handleHvacPowerOn(boolean isOn) { boolean shouldPropagate = mDataStore.shouldPropagateHvacPowerUpdate(isOn); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Hvac Power On: " + isOn + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onHvacPowerChange(isOn); } } } } void handleSeatWarmerUpdate(int zone, int level) { boolean shouldPropagate = mDataStore.shouldPropagateSeatWarmerLevelUpdate(zone, level); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Seat Warmer Update, zone: " + zone + " level: " + level + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { if (zone == VehicleAreaSeat.SEAT_ROW_1_LEFT) { mCallbacks.get(i).onDriverSeatWarmerChange(level); } else { mCallbacks.get(i).onPassengerSeatWarmerChange(level); } } } } } private void handleAirCirculationUpdate(boolean airCirculationState) { boolean shouldPropagate = mDataStore.shouldPropagateAirCirculationUpdate(airCirculationState); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Air Circulation Update: " + airCirculationState + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onAirCirculationChange(airCirculationState); } } } } private void handleAutoModeUpdate(boolean autoModeState) { boolean shouldPropagate = mDataStore.shouldPropagateAutoModeUpdate(autoModeState); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "AutoMode Update, id: " + autoModeState + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onAutoModeChange(autoModeState); } } } } private void handleAcStateUpdate(boolean acState) { boolean shouldPropagate = mDataStore.shouldPropagateAcUpdate(acState); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "AC State Update, id: " + acState + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onAcStateChange(acState); } } } } private void handleFanPositionUpdate(int zone, int position) { int index = fanPositionToAirflowIndex(position); boolean shouldPropagate = mDataStore.shouldPropagateFanPositionUpdate(zone, index); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Fan Position Update, zone: " + zone + " position: " + position + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onFanDirectionChange(position); } } } } private void handleFanSpeedUpdate(int zone, int speed) { boolean shouldPropagate = mDataStore.shouldPropagateFanSpeedUpdate(zone, speed); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Fan Speed Update, zone: " + zone + " speed: " + speed + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onFanSpeedChange(speed); } } } } private void handleTempUpdate(CarPropertyValue value) { final int zone = value.getAreaId(); final float temp = (Float) value.getValue(); final boolean available = value.getStatus() == CarPropertyValue.STATUS_AVAILABLE; boolean shouldPropagate = mDataStore.shouldPropagateTempUpdate(zone, temp, available); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Temp Update, zone: " + zone + " temp: " + temp + "available: " + available + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { if (zone == DRIVER_ZONE_ID) { mCallbacks.get(i) .onDriverTemperatureChange(value); } else if (zone == PASSENGER_ZONE_ID) { mCallbacks.get(i) .onPassengerTemperatureChange(value); } else { Log.w(TAG, "Unknown temperature set area id: " + zone); } } } } } private void handleDefrosterUpdate(int zone, boolean defrosterState) { boolean shouldPropagate = mDataStore.shouldPropagateDefrosterUpdate(zone, defrosterState); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Defroster Update, zone: " + zone + " state: " + defrosterState + " should propagate: " + shouldPropagate); } if (shouldPropagate) { synchronized (mCallbacks) { for (int i = 0; i < mCallbacks.size(); i++) { if (zone == VehicleAreaWindow.WINDOW_FRONT_WINDSHIELD) { mCallbacks.get(i).onFrontDefrosterChange(defrosterState); } else if (zone == VehicleAreaWindow.WINDOW_REAR_WINDSHIELD) { mCallbacks.get(i).onRearDefrosterChange(defrosterState); } } } } } public void requestRefresh(final Runnable r, final Handler h) { final AsyncTask task = new AsyncTask() { @Override protected Void doInBackground(Void... unused) { synchronized (mHvacManagerReady) { while (mHvacManager == null) { try { mHvacManagerReady.wait(); } catch (InterruptedException e) { // We got interrupted so we might be shutting down. return null; } } } fetchTemperature(DRIVER_ZONE_ID); fetchTemperature(PASSENGER_ZONE_ID); fetchFanSpeed(); fetchDefrosterState(VehicleAreaWindow.WINDOW_FRONT_WINDSHIELD); fetchDefrosterState(VehicleAreaWindow.WINDOW_REAR_WINDSHIELD); fetchAirflow(); fetchAcState(); fetchAirCirculation(); fetchHvacPowerState(); return null; } @Override protected void onPostExecute(Void unused) { h.post(r); } }; task.execute(); } public HvacPolicy getPolicy() { return mPolicy; } public boolean isTemperatureControlAvailable(int zone) { if (mHvacManager != null) { try { return mHvacManager.isPropertyAvailable( CarHvacManager.ID_ZONED_TEMP_SETPOINT, zone); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in isTemperatureControlAvailable"); } } return false; } public boolean isDriverTemperatureControlAvailable() { return isTemperatureControlAvailable(DRIVER_ZONE_ID); } public boolean isPassengerTemperatureControlAvailable() { return isTemperatureControlAvailable(PASSENGER_ZONE_ID); } private void fetchTemperature(int zone) { if (mHvacManager != null) { try { float value = mHvacManager.getFloatProperty( CarHvacManager.ID_ZONED_TEMP_SETPOINT, zone); boolean available = mHvacManager.isPropertyAvailable( CarHvacManager.ID_ZONED_TEMP_SETPOINT, zone); mDataStore.setTemperature(zone, value, available); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchTemperature"); } } } public int getDriverTemperature() { return Float.valueOf(mDataStore.getTemperature(DRIVER_ZONE_ID)).intValue(); } public int getPassengerTemperature() { return Float.valueOf(mDataStore.getTemperature(PASSENGER_ZONE_ID)).intValue(); } public void setDriverTemperature(int temperature) { setTemperature(DRIVER_ZONE_ID, temperature); } public void setPassengerTemperature(int temperature) { setTemperature(PASSENGER_ZONE_ID, temperature); } public void setTemperature(final int zone, final float temperature) { final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setFloatProperty( CarHvacManager.ID_ZONED_TEMP_SETPOINT, zone, temperature); // if the set() succeeds, consider the property available mDataStore.setTemperature(zone, temperature, true); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setTemperature"); } catch (Exception e) { Log.e(TAG, "set temp failed", e); } } return null; } }; task.execute(); } public void setHvacPowerState(final boolean state) { final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setBooleanProperty(CarHvacManager.ID_ZONED_HVAC_POWER_ON, SEAT_ALL, state); // if the set() succeeds, consider the property available mDataStore.setHvacPowerState(state); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setHvacPowerState"); } catch (Exception e) { Log.e(TAG, "set power failed", e); } } return null; } }; task.execute(); } public void setDriverSeatWarmerLevel(int level) { setSeatWarmerLevel(VehicleAreaSeat.SEAT_ROW_1_LEFT, level); } public void setPassengerSeatWarmerLevel(int level) { setSeatWarmerLevel(VehicleAreaSeat.SEAT_ROW_1_RIGHT, level); } public void setSeatWarmerLevel(final int zone, final int level) { mDataStore.setSeatWarmerLevel(zone, level); final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setIntProperty( CarHvacManager.ID_ZONED_SEAT_TEMP, zone, level); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setSeatWarmerLevel"); } catch (Exception e) { Log.e(TAG, "set seat warmer failed", e); } } return null; } }; task.execute(); } private void fetchFanSpeed() { if (mHvacManager != null) { int zone = SEAT_ALL; // Car specific workaround. try { mDataStore.setFanSpeed(mHvacManager.getIntProperty( CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchFanSpeed"); } } } public int getFanSpeed() { return mDataStore.getFanSpeed(); } public void setFanSpeed(final int fanSpeed) { mDataStore.setFanSpeed(fanSpeed); final AsyncTask task = new AsyncTask() { int newFanSpeed; protected Void doInBackground(Void... unused) { if (mHvacManager != null) { int zone = SEAT_ALL; // Car specific workaround. try { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Setting fanspeed to: " + fanSpeed); } mHvacManager.setIntProperty( CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone, fanSpeed); newFanSpeed = mHvacManager.getIntProperty( CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setFanSpeed"); } } return null; } @Override protected void onPostExecute(final Void result) { Log.e(TAG, "postExecute new fanSpeed: " + newFanSpeed); } }; task.execute(); } private void fetchDefrosterState(int zone) { if (mHvacManager != null) { try { mDataStore.setDefrosterState(zone, mHvacManager.getBooleanProperty( CarHvacManager.ID_WINDOW_DEFROSTER_ON, zone)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchDefrosterState"); } } } public boolean getFrontDefrosterState() { return mDataStore.getDefrosterState(VehicleAreaWindow.WINDOW_FRONT_WINDSHIELD); } public boolean getRearDefrosterState() { return mDataStore.getDefrosterState(VehicleAreaWindow.WINDOW_REAR_WINDSHIELD); } public void setFrontDefrosterState(boolean state) { setDefrosterState(VehicleAreaWindow.WINDOW_FRONT_WINDSHIELD, state); } public void setRearDefrosterState(boolean state) { setDefrosterState(VehicleAreaWindow.WINDOW_REAR_WINDSHIELD, state); } public void setDefrosterState(final int zone, final boolean state) { mDataStore.setDefrosterState(zone, state); final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setBooleanProperty( CarHvacManager.ID_WINDOW_DEFROSTER_ON, zone, state); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setDeforsterState"); } } return null; } }; task.execute(); } private void fetchAcState() { if (mHvacManager != null) { try { mDataStore.setAcState(mHvacManager.getBooleanProperty(CarHvacManager.ID_ZONED_AC_ON, SEAT_ALL)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchAcState"); } } } public boolean getAcState() { return mDataStore.getAcState(); } public void setAcState(final boolean state) { mDataStore.setAcState(state); final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setBooleanProperty(CarHvacManager.ID_ZONED_AC_ON, SEAT_ALL, state); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setAcState"); } } return null; } }; task.execute(); } private int fanPositionToAirflowIndex(int fanPosition) { for (int i = 0; i < AIRFLOW_STATES.length; i++) { if (fanPosition == AIRFLOW_STATES[i]) { return i; } } Log.e(TAG, "Unknown fan position " + fanPosition + ". Returning default."); return AIRFLOW_STATES[0]; } private void fetchAirflow() { if (mHvacManager != null) { try { int val = mHvacManager.getIntProperty(CarHvacManager.ID_ZONED_FAN_DIRECTION, SEAT_ALL); mDataStore.setAirflow(SEAT_ALL, fanPositionToAirflowIndex(val)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchAirFlow"); } } } public int getAirflowIndex(int zone) { return mDataStore.getAirflow(zone); } public void setAirflowIndex(final int zone, final int index) { mDataStore.setAirflow(zone, index); int override = SEAT_ALL; // Car specific workaround. int val = AIRFLOW_STATES[index]; setFanDirection(override, val); } public void setFanDirection(final int direction) { mDataStore.setAirflow(SEAT_ALL, direction); setFanDirection(SEAT_ALL, direction); } private void setFanDirection(final int zone, final int direction) { final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setIntProperty( CarHvacManager.ID_ZONED_FAN_DIRECTION, zone, direction); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setAirflowIndex"); } } return null; } }; task.execute(); } private void fetchAirCirculation() { if (mHvacManager != null) { try { mDataStore.setAirCirculationState(mHvacManager .getBooleanProperty(CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON, SEAT_ALL)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchAirCirculationState"); } } } public boolean getAirCirculationState() { return mDataStore.getAirCirculationState(); } public void setAirCirculation(final boolean state) { mDataStore.setAirCirculationState(state); final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setBooleanProperty( CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON, SEAT_ALL, state); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setAcState"); } } return null; } }; task.execute(); } public boolean getAutoModeState() { return mDataStore.getAutoModeState(); } public void setAutoMode(final boolean state) { mDataStore.setAutoModeState(state); final AsyncTask task = new AsyncTask() { protected Void doInBackground(Void... unused) { if (mHvacManager != null) { try { mHvacManager.setBooleanProperty(CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON, SEAT_ALL, state); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in setAutoModeState"); } } return null; } }; task.execute(); } public boolean getHvacPowerState() { return mDataStore.getHvacPowerState(); } private void fetchHvacPowerState() { if (mHvacManager != null) { try { mDataStore.setHvacPowerState(mHvacManager.getBooleanProperty( CarHvacManager.ID_ZONED_HVAC_POWER_ON, SEAT_ALL)); } catch (android.car.CarNotConnectedException e) { Log.e(TAG, "Car not connected in fetchHvacPowerState"); } } } }