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