• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2016, 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 package com.android.car.hvac;
17 
18 import android.app.Service;
19 import android.car.Car;
20 import android.content.BroadcastReceiver;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.ServiceConnection;
26 import android.content.res.Resources;
27 import android.graphics.PixelFormat;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.UserHandle;
31 import android.util.DisplayMetrics;
32 import android.util.Log;
33 import android.view.Gravity;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.WindowManager;
37 
38 import com.android.car.hvac.controllers.HvacPanelController;
39 import com.android.car.hvac.ui.TemperatureBarOverlay;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 
45 /**
46  * Creates a sliding panel for HVAC controls and adds it to the window manager above SystemUI.
47  */
48 public class HvacUiService extends Service {
49     public static final String CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS =
50             "android.car.intent.action.TOGGLE_HVAC_CONTROLS";
51     private static final String TAG = "HvacUiService";
52 
53     private final List<View> mAddedViews = new ArrayList<>();
54 
55     private WindowManager mWindowManager;
56 
57     private View mContainer;
58 
59     private int mNavBarHeight;
60     private int mPanelCollapsedHeight;
61     private int mPanelFullExpandedHeight;
62     private int mScreenBottom;
63     private int mScreenWidth;
64     // This is to compensate for the difference between where the y coordinate origin is and that
65     // of the actual bottom of the screen.
66     private int mInitialYOffset = 0;
67     private DisplayMetrics mDisplayMetrics;
68 
69     private int mTemperatureSideMargin;
70     private int mTemperatureOverlayWidth;
71     private int mTemperatureOverlayHeight;
72 
73     private HvacPanelController mHvacPanelController;
74     private HvacController mHvacController;
75 
76     // we need both a expanded and collapsed version due to a rendering bug during window resize
77     // thus instead we swap between the collapsed window and the expanded one before/after they
78     // are needed.
79     private TemperatureBarOverlay mDriverTemperatureBar;
80     private TemperatureBarOverlay mPassengerTemperatureBar;
81     private TemperatureBarOverlay mDriverTemperatureBarCollapsed;
82     private TemperatureBarOverlay mPassengerTemperatureBarCollapsed;
83 
84 
85     @Override
onBind(Intent intent)86     public IBinder onBind(Intent intent) {
87         throw new UnsupportedOperationException("Not yet implemented.");
88     }
89 
90     @Override
onCreate()91     public void onCreate() {
92         Resources res = getResources();
93         boolean showCollapsed = res.getBoolean(R.bool.config_showCollapsedBars);
94         mPanelCollapsedHeight = res.getDimensionPixelSize(R.dimen.car_hvac_panel_collapsed_height);
95         mPanelFullExpandedHeight
96                 = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);
97 
98         mTemperatureSideMargin = res.getDimensionPixelSize(R.dimen.temperature_side_margin);
99         mTemperatureOverlayWidth =
100                 res.getDimensionPixelSize(R.dimen.temperature_bar_width_expanded);
101         mTemperatureOverlayHeight
102                 = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);
103 
104         mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
105 
106         mDisplayMetrics = new DisplayMetrics();
107         mWindowManager.getDefaultDisplay().getRealMetrics(mDisplayMetrics);
108         mScreenBottom = mDisplayMetrics.heightPixels;
109         mScreenWidth = mDisplayMetrics.widthPixels;
110 
111         int identifier = res.getIdentifier("navigation_bar_height_car_mode", "dimen", "android");
112         mNavBarHeight = (identifier > 0 && showCollapsed) ?
113                 res.getDimensionPixelSize(identifier) : 0;
114 
115         WindowManager.LayoutParams testparams = new WindowManager.LayoutParams(
116                 WindowManager.LayoutParams.MATCH_PARENT,
117                 WindowManager.LayoutParams.MATCH_PARENT,
118                 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
119                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
120                         | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
121                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
122                 PixelFormat.TRANSLUCENT);
123 
124         // There does not exist a way to get the current state of the system ui visibility from
125         // inside a Service thus we place something that's full screen and check it's final
126         // measurements as a hack to get that information. Once we have the initial state  we can
127         // safely just register for the change events from that point on.
128         View windowSizeTest = new View(this) {
129             @Override
130             protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
131                 boolean sysUIShowing = (mDisplayMetrics.heightPixels != bottom);
132                 mInitialYOffset = (sysUIShowing) ? -mNavBarHeight : 0;
133                 layoutHvacUi();
134                 // we now have initial state so this empty view is not longer needed.
135                 mWindowManager.removeView(this);
136                 mAddedViews.remove(this);
137             }
138         };
139         addViewToWindowManagerAndTrack(windowSizeTest, testparams);
140         IntentFilter filter = new IntentFilter();
141         filter.addAction(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS);
142         // Register receiver such that any user with climate control permission can call it.
143         registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
144                 Car.PERMISSION_CONTROL_CAR_CLIMATE, null);
145     }
146 
147 
148     /**
149      * Called after the mInitialYOffset is determined. This does a layout of all components needed
150      * for the HVAC UI. On start the all the windows need for the collapsed view are visible whereas
151      * the expanded view's windows are created and sized but are invisible.
152      */
layoutHvacUi()153     private void layoutHvacUi() {
154         LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
155         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
156                 WindowManager.LayoutParams.WRAP_CONTENT,
157                 WindowManager.LayoutParams.WRAP_CONTENT,
158                 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
159                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
160                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
161                         & ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
162                 PixelFormat.TRANSLUCENT);
163 
164         params.packageName = this.getPackageName();
165         params.gravity = Gravity.BOTTOM | Gravity.LEFT;
166 
167         params.x = 0;
168         params.y = mInitialYOffset;
169 
170         params.width = mScreenWidth;
171         params.height = mScreenBottom;
172         params.setTitle("HVAC Container");
173         disableAnimations(params);
174         // required of the sysui visiblity listener is not triggered.
175         params.hasSystemUiListeners = true;
176 
177         mContainer = inflater.inflate(R.layout.hvac_panel, null);
178         mContainer.setLayoutParams(params);
179         mContainer.setOnSystemUiVisibilityChangeListener(visibility -> {
180             boolean systemUiVisible = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
181             int y = 0;
182             if (systemUiVisible) {
183                 // when the system ui is visible the windowing systems coordinates start with
184                 // 0 being above the system navigation bar. Therefore if we want to get the the
185                 // actual bottom of the screen we need to set the y value to negative value of the
186                 // navigation bar height.
187                 y = -mNavBarHeight;
188             }
189             setYPosition(mDriverTemperatureBar, y);
190             setYPosition(mPassengerTemperatureBar, y);
191             setYPosition(mDriverTemperatureBarCollapsed, y);
192             setYPosition(mPassengerTemperatureBarCollapsed, y);
193             setYPosition(mContainer, y);
194         });
195 
196         // The top padding should be calculated on the screen height and the height of the
197         // expanded hvac panel. The space defined by the padding is meant to be clickable for
198         // dismissing the hvac panel.
199         int topPadding = mScreenBottom - mPanelFullExpandedHeight;
200         mContainer.setPadding(0, topPadding, 0, 0);
201 
202         mContainer.setFocusable(false);
203         mContainer.setFocusableInTouchMode(false);
204 
205         View panel = mContainer.findViewById(R.id.hvac_center_panel);
206         panel.getLayoutParams().height = mPanelCollapsedHeight;
207 
208         addViewToWindowManagerAndTrack(mContainer, params);
209 
210         createTemperatureBars(inflater);
211         mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
212                 mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
213                 mDriverTemperatureBarCollapsed, mPassengerTemperatureBarCollapsed
214         );
215         Intent bindIntent = new Intent(this /* context */, HvacController.class);
216         if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
217             Log.e(TAG, "Failed to connect to HvacController.");
218         }
219     }
220 
addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params)221     private void addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params) {
222         mWindowManager.addView(view, params);
223         mAddedViews.add(view);
224     }
225 
setYPosition(View v, int y)226     private void setYPosition(View v, int y) {
227         WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams();
228         lp.y = y;
229         mWindowManager.updateViewLayout(v, lp);
230     }
231 
232     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
233         @Override
234         public void onReceive(Context context, Intent intent) {
235             String action = intent.getAction();
236             if(action.equals(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS)){
237                 mHvacPanelController.toggleHvacUi();
238             }
239         }
240     };
241 
242     @Override
onDestroy()243     public void onDestroy() {
244         for (View view : mAddedViews) {
245             mWindowManager.removeView(view);
246         }
247         mAddedViews.clear();
248         if(mHvacController != null){
249             unbindService(mServiceConnection);
250         }
251         unregisterReceiver(mBroadcastReceiver);
252     }
253 
254     private ServiceConnection mServiceConnection = new ServiceConnection() {
255         @Override
256         public void onServiceConnected(ComponentName className, IBinder service) {
257             mHvacController = ((HvacController.LocalBinder) service).getService();
258             final Context context = HvacUiService.this;
259 
260             final Runnable r = () -> {
261                 // Once the hvac controller has refreshed its values from the vehicle,
262                 // bind all the values.
263                 mHvacPanelController.updateHvacController(mHvacController);
264             };
265 
266             if (mHvacController != null) {
267                 mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
268             }
269         }
270 
271         @Override
272         public void onServiceDisconnected(ComponentName className) {
273             mHvacController = null;
274             mHvacPanelController.updateHvacController(null);
275             //TODO: b/29126575 reconnect to controller if it is restarted
276         }
277     };
278 
createTemperatureBars(LayoutInflater inflater)279     private void createTemperatureBars(LayoutInflater inflater) {
280         mDriverTemperatureBarCollapsed = createTemperatureBarOverlay(inflater,
281                 "HVAC Driver Temp collapsed",
282                 mNavBarHeight,
283                 Gravity.BOTTOM | Gravity.LEFT);
284 
285         mPassengerTemperatureBarCollapsed = createTemperatureBarOverlay(inflater,
286                 "HVAC Passenger Temp collapsed",
287                 mNavBarHeight,
288                 Gravity.BOTTOM | Gravity.RIGHT);
289 
290         mDriverTemperatureBar = createTemperatureBarOverlay(inflater,
291                 "HVAC Driver Temp",
292                 mTemperatureOverlayHeight,
293                 Gravity.BOTTOM | Gravity.LEFT);
294 
295         mPassengerTemperatureBar = createTemperatureBarOverlay(inflater,
296                 "HVAC Passenger Temp",
297                 mTemperatureOverlayHeight,
298                 Gravity.BOTTOM | Gravity.RIGHT);
299     }
300 
createTemperatureBarOverlay(LayoutInflater inflater, String windowTitle, int windowHeight, int gravity)301     private TemperatureBarOverlay createTemperatureBarOverlay(LayoutInflater inflater,
302             String windowTitle, int windowHeight, int gravity) {
303         WindowManager.LayoutParams params = createTemperatureBarLayoutParams(
304                 windowTitle, windowHeight, gravity);
305         TemperatureBarOverlay button = (TemperatureBarOverlay) inflater
306                 .inflate(R.layout.hvac_temperature_bar_overlay, null);
307         button.setLayoutParams(params);
308         addViewToWindowManagerAndTrack(button, params);
309         return button;
310     }
311 
312     // note the window manager does not copy the layout params but uses the supplied object thus
313     // you need a new copy for each window or change 1 can effect the others
createTemperatureBarLayoutParams(String windowTitle, int windowHeight, int gravity)314     private WindowManager.LayoutParams createTemperatureBarLayoutParams(String windowTitle,
315             int windowHeight, int gravity) {
316         WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
317                 WindowManager.LayoutParams.WRAP_CONTENT,
318                 WindowManager.LayoutParams.WRAP_CONTENT,
319                 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
320                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
321                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
322                 PixelFormat.TRANSLUCENT);
323         lp.x = mTemperatureSideMargin;
324         lp.y = mInitialYOffset;
325         lp.width = mTemperatureOverlayWidth;
326         disableAnimations(lp);
327         lp.setTitle(windowTitle);
328         lp.height = windowHeight;
329         lp.gravity = gravity;
330         return lp;
331     }
332 
333     /**
334      * Disables animations when window manager updates a child view.
335      */
disableAnimations(WindowManager.LayoutParams params)336     private void disableAnimations(WindowManager.LayoutParams params) {
337         try {
338             int currentFlags = (Integer) params.getClass().getField("privateFlags").get(params);
339             params.getClass().getField("privateFlags").set(params, currentFlags | 0x00000040);
340         } catch (Exception e) {
341             Log.e(TAG, "Error disabling animation");
342         }
343     }
344 }
345