1 /* 2 * Copyright (C) 2021 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.systemui.car.hvac; 18 19 import static android.car.VehiclePropertyIds.HVAC_POWER_ON; 20 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET; 21 22 import static com.android.systemui.car.hvac.HvacUtils.celsiusToFahrenheit; 23 import static com.android.systemui.car.hvac.HvacUtils.fahrenheitToCelsius; 24 25 import android.car.hardware.CarPropertyValue; 26 import android.content.Context; 27 import android.content.res.TypedArray; 28 import android.util.AttributeSet; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.widget.LinearLayout; 32 import android.widget.TextView; 33 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.core.content.ContextCompat; 37 38 import com.android.systemui.R; 39 40 /** 41 * An implementation of HvacView that displays the {@code HVAC_TEMPERATURE_SET} and two buttons to 42 * increase or decrease {@code HVAC_TEMPERATURE_SET}. 43 */ 44 public class TemperatureControlView extends LinearLayout implements HvacView { 45 protected static final int BUTTON_REPEAT_INTERVAL_MS = 500; 46 47 private static final int INVALID_ID = -1; 48 49 private final int mAreaId; 50 private final int mAvailableTextColor; 51 private final int mUnavailableTextColor; 52 53 private boolean mPowerOn; 54 private boolean mTemperatureSetAvailable; 55 private HvacPropertySetter mHvacPropertySetter; 56 private TextView mTempTextView; 57 private String mTempInDisplay; 58 private View mIncreaseButton; 59 private View mDecreaseButton; 60 private float mMinTempC; 61 private float mMaxTempC; 62 private String mTemperatureFormatCelsius; 63 private String mTemperatureFormatFahrenheit; 64 private int mTemperatureIncrementFractionCelsius; 65 private int mTemperatureIncrementFractionFahrenheit; 66 private float mTemperatureIncrementCelsius; 67 private float mTemperatureIncrementFahrenheit; 68 private float mCurrentTempC; 69 private boolean mDisplayInFahrenheit; 70 TemperatureControlView(Context context, @Nullable AttributeSet attrs)71 public TemperatureControlView(Context context, @Nullable AttributeSet attrs) { 72 super(context, attrs); 73 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HvacView); 74 mAreaId = typedArray.getInt(R.styleable.HvacView_hvacAreaId, INVALID_ID); 75 mTemperatureFormatCelsius = getResources().getString( 76 R.string.hvac_temperature_format_celsius); 77 mTemperatureFormatFahrenheit = getResources().getString( 78 R.string.hvac_temperature_format_fahrenheit); 79 mTemperatureIncrementFractionCelsius = getResources().getInteger( 80 R.integer.celsius_increment_fraction); 81 mTemperatureIncrementFractionFahrenheit = getResources().getInteger( 82 R.integer.fahrenheit_increment_fraction); 83 mTemperatureIncrementCelsius = 84 1f / mTemperatureIncrementFractionCelsius; 85 mTemperatureIncrementFahrenheit = 86 1f / mTemperatureIncrementFractionFahrenheit; 87 88 mMinTempC = getResources().getFloat(R.dimen.hvac_min_value_celsius); 89 mMaxTempC = getResources().getFloat(R.dimen.hvac_max_value_celsius); 90 mAvailableTextColor = ContextCompat.getColor(getContext(), R.color.system_bar_text_color); 91 mUnavailableTextColor = ContextCompat.getColor(getContext(), 92 R.color.system_bar_text_unavailable_color); 93 } 94 95 @Override onFinishInflate()96 public void onFinishInflate() { 97 super.onFinishInflate(); 98 mTempTextView = requireViewById(R.id.hvac_temperature_text); 99 mIncreaseButton = requireViewById(R.id.hvac_increase_button); 100 mDecreaseButton = requireViewById(R.id.hvac_decrease_button); 101 initButtons(); 102 } 103 104 @Override onHvacTemperatureUnitChanged(boolean usesFahrenheit)105 public void onHvacTemperatureUnitChanged(boolean usesFahrenheit) { 106 mDisplayInFahrenheit = usesFahrenheit; 107 updateTemperatureView(); 108 } 109 110 @Override onPropertyChanged(CarPropertyValue value)111 public void onPropertyChanged(CarPropertyValue value) { 112 if (value.getPropertyId() == HVAC_TEMPERATURE_SET) { 113 mCurrentTempC = (Float) value.getValue(); 114 mTemperatureSetAvailable = value.getStatus() == CarPropertyValue.STATUS_AVAILABLE; 115 } 116 117 if (value.getPropertyId() == HVAC_POWER_ON) { 118 mPowerOn = (Boolean) value.getValue(); 119 } 120 updateTemperatureView(); 121 } 122 123 @Override getHvacPropertyToView()124 public @HvacController.HvacProperty Integer getHvacPropertyToView() { 125 return HVAC_TEMPERATURE_SET; 126 } 127 128 @Override getAreaId()129 public @HvacController.AreaId Integer getAreaId() { 130 return mAreaId; 131 } 132 133 @Override setHvacPropertySetter(HvacPropertySetter hvacPropertySetter)134 public void setHvacPropertySetter(HvacPropertySetter hvacPropertySetter) { 135 mHvacPropertySetter = hvacPropertySetter; 136 } 137 138 /** 139 * Returns {@code true} if temperature should be available for change. 140 */ isTemperatureAvailableForChange()141 public boolean isTemperatureAvailableForChange() { 142 return mPowerOn && mTemperatureSetAvailable && mHvacPropertySetter != null; 143 } 144 145 /** 146 * Updates the temperature view logic on the UI thread. 147 */ updateTemperatureViewUiThread()148 protected void updateTemperatureViewUiThread() { 149 mTempTextView.setText(mTempInDisplay); 150 mTempTextView.setTextColor(mPowerOn && mTemperatureSetAvailable 151 ? mAvailableTextColor : mUnavailableTextColor); 152 } 153 getTempInDisplay()154 protected String getTempInDisplay() { 155 return mTempInDisplay; 156 } 157 getCurrentTempC()158 protected float getCurrentTempC() { 159 return mCurrentTempC; 160 } 161 162 @VisibleForTesting getTempFormatInFahrenheit()163 String getTempFormatInFahrenheit() { 164 return mTemperatureFormatFahrenheit; 165 } 166 167 @VisibleForTesting getTempFormatInCelsius()168 String getTempFormatInCelsius() { 169 return mTemperatureFormatCelsius; 170 } 171 172 @VisibleForTesting getTemperatureIncrementInCelsius()173 float getTemperatureIncrementInCelsius() { 174 return mTemperatureIncrementCelsius; 175 } 176 177 @VisibleForTesting getTemperatureIncrementInFahrenheit()178 float getTemperatureIncrementInFahrenheit() { 179 return mTemperatureIncrementFahrenheit; 180 } 181 initButtons()182 private void initButtons() { 183 mIncreaseButton.setOnClickListener((v) -> incrementTemperature(true)); 184 mDecreaseButton.setOnClickListener((v) -> incrementTemperature(false)); 185 186 setHoldToRepeatButton(mIncreaseButton); 187 setHoldToRepeatButton(mDecreaseButton); 188 } 189 incrementTemperature(boolean increment)190 private void incrementTemperature(boolean increment) { 191 if (!mPowerOn) return; 192 193 float newTempC; 194 if (mDisplayInFahrenheit) { 195 float currentTempF = celsiusToFahrenheit(mCurrentTempC); 196 float newTempF = increment 197 ? currentTempF + mTemperatureIncrementFahrenheit 198 : currentTempF - mTemperatureIncrementFahrenheit; 199 newTempC = fahrenheitToCelsius(newTempF); 200 } else { 201 newTempC = increment 202 ? mCurrentTempC + mTemperatureIncrementCelsius 203 : mCurrentTempC - mTemperatureIncrementCelsius; 204 } 205 206 setTemperature(newTempC); 207 } 208 updateTemperatureView()209 private void updateTemperatureView() { 210 float tempToDisplayUnformatted = roundToClosestFraction( 211 mDisplayInFahrenheit ? celsiusToFahrenheit(mCurrentTempC) : mCurrentTempC); 212 // Set mCurrentTempC value to tempToDisplayUnformatted so their values sync in the next 213 // setTemperature call. 214 mCurrentTempC = mDisplayInFahrenheit 215 ? fahrenheitToCelsius(tempToDisplayUnformatted) 216 : tempToDisplayUnformatted; 217 218 mTempInDisplay = String.format( 219 mDisplayInFahrenheit ? mTemperatureFormatFahrenheit : mTemperatureFormatCelsius, 220 tempToDisplayUnformatted); 221 mContext.getMainExecutor().execute(this::updateTemperatureViewUiThread); 222 } 223 setTemperature(float tempC)224 private void setTemperature(float tempC) { 225 tempC = Math.min(tempC, mMaxTempC); 226 tempC = Math.max(tempC, mMinTempC); 227 if (isTemperatureAvailableForChange()) { 228 mHvacPropertySetter.setHvacProperty(HVAC_TEMPERATURE_SET, mAreaId, tempC); 229 } 230 } 231 232 /** 233 * Configures the {@code button} to perform its click action repeatedly if pressed and held with 234 * {@link #BUTTON_REPEAT_INTERVAL_MS}. 235 */ setHoldToRepeatButton(View button)236 private void setHoldToRepeatButton(View button) { 237 Runnable repeatClickRunnable = new Runnable() { 238 @Override 239 public void run() { 240 button.performClick(); 241 mContext.getMainThreadHandler().postDelayed(this, BUTTON_REPEAT_INTERVAL_MS); 242 } 243 }; 244 245 button.setOnTouchListener((view, event) -> { 246 int action = event.getAction(); 247 switch (action) { 248 case MotionEvent.ACTION_DOWN: 249 // Handle click action here since click listener is suppressed. 250 repeatClickRunnable.run(); 251 break; 252 case MotionEvent.ACTION_UP: 253 case MotionEvent.ACTION_CANCEL: 254 mContext.getMainThreadHandler().removeCallbacks(repeatClickRunnable); 255 } 256 257 // Return true so on click listener is not called superfluously. 258 return true; 259 }); 260 } 261 roundToClosestFraction(float rawFloat)262 private float roundToClosestFraction(float rawFloat) { 263 float incrementFraction = mDisplayInFahrenheit 264 ? mTemperatureIncrementFractionFahrenheit 265 : mTemperatureIncrementFractionCelsius; 266 return Math.round(rawFloat * incrementFraction) / incrementFraction; 267 } 268 } 269