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