• 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         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
143         // Register receiver such that any user with climate control permission can call it.
144         registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
145                 Car.PERMISSION_CONTROL_CAR_CLIMATE, null);
146     }
147 
148 
149     /**
150      * Called after the mInitialYOffset is determined. This does a layout of all components needed
151      * for the HVAC UI. On start the all the windows need for the collapsed view are visible whereas
152      * the expanded view's windows are created and sized but are invisible.
153      */
layoutHvacUi()154     private void layoutHvacUi() {
155         LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
156         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
157                 WindowManager.LayoutParams.WRAP_CONTENT,
158                 WindowManager.LayoutParams.WRAP_CONTENT,
159                 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
160                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
161                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
162                         & ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
163                 PixelFormat.TRANSLUCENT);
164 
165         params.packageName = this.getPackageName();
166         params.gravity = Gravity.BOTTOM | Gravity.LEFT;
167 
168         params.x = 0;
169         params.y = mInitialYOffset;
170 
171         params.width = mScreenWidth;
172         params.height = mScreenBottom;
173         params.setTitle("HVAC Container");
174         disableAnimations(params);
175         // required of the sysui visiblity listener is not triggered.
176         params.hasSystemUiListeners = true;
177 
178         mContainer = inflater.inflate(R.layout.hvac_panel, null);
179         mContainer.setLayoutParams(params);
180         mContainer.setOnSystemUiVisibilityChangeListener(visibility -> {
181             boolean systemUiVisible = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
182             int y = 0;
183             if (systemUiVisible) {
184                 // when the system ui is visible the windowing systems coordinates start with
185                 // 0 being above the system navigation bar. Therefore if we want to get the the
186                 // actual bottom of the screen we need to set the y value to negative value of the
187                 // navigation bar height.
188                 y = -mNavBarHeight;
189             }
190             setYPosition(mDriverTemperatureBar, y);
191             setYPosition(mPassengerTemperatureBar, y);
192             setYPosition(mDriverTemperatureBarCollapsed, y);
193             setYPosition(mPassengerTemperatureBarCollapsed, y);
194             setYPosition(mContainer, y);
195         });
196 
197         // The top padding should be calculated on the screen height and the height of the
198         // expanded hvac panel. The space defined by the padding is meant to be clickable for
199         // dismissing the hvac panel.
200         int topPadding = mScreenBottom - mPanelFullExpandedHeight;
201         mContainer.setPadding(0, topPadding, 0, 0);
202 
203         mContainer.setFocusable(false);
204         mContainer.setFocusableInTouchMode(false);
205 
206         View panel = mContainer.findViewById(R.id.hvac_center_panel);
207         panel.getLayoutParams().height = mPanelCollapsedHeight;
208 
209         addViewToWindowManagerAndTrack(mContainer, params);
210 
211         createTemperatureBars(inflater);
212         mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
213                 mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
214                 mDriverTemperatureBarCollapsed, mPassengerTemperatureBarCollapsed
215         );
216         Intent bindIntent = new Intent(this /* context */, HvacController.class);
217         if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
218             Log.e(TAG, "Failed to connect to HvacController.");
219         }
220     }
221 
addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params)222     private void addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params) {
223         mWindowManager.addView(view, params);
224         mAddedViews.add(view);
225     }
226 
setYPosition(View v, int y)227     private void setYPosition(View v, int y) {
228         WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams();
229         lp.y = y;
230         mWindowManager.updateViewLayout(v, lp);
231     }
232 
233     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
234         @Override
235         public void onReceive(Context context, Intent intent) {
236             String action = intent.getAction();
237             if (action.equals(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS)){
238                 mHvacPanelController.toggleHvacUi();
239             } else if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
240                 mHvacPanelController.collapseHvacUi();
241             }
242         }
243     };
244 
245     @Override
onDestroy()246     public void onDestroy() {
247         for (View view : mAddedViews) {
248             mWindowManager.removeView(view);
249         }
250         mAddedViews.clear();
251         if(mHvacController != null){
252             unbindService(mServiceConnection);
253         }
254         unregisterReceiver(mBroadcastReceiver);
255     }
256 
257     private ServiceConnection mServiceConnection = new ServiceConnection() {
258         @Override
259         public void onServiceConnected(ComponentName className, IBinder service) {
260             mHvacController = ((HvacController.LocalBinder) service).getService();
261             final Context context = HvacUiService.this;
262 
263             final Runnable r = () -> {
264                 // Once the hvac controller has refreshed its values from the vehicle,
265                 // bind all the values.
266                 mHvacPanelController.updateHvacController(mHvacController);
267             };
268 
269             if (mHvacController != null) {
270                 mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
271             }
272         }
273 
274         @Override
275         public void onServiceDisconnected(ComponentName className) {
276             mHvacController = null;
277             mHvacPanelController.updateHvacController(null);
278             //TODO: b/29126575 reconnect to controller if it is restarted
279         }
280     };
281 
createTemperatureBars(LayoutInflater inflater)282     private void createTemperatureBars(LayoutInflater inflater) {
283         mDriverTemperatureBarCollapsed = createTemperatureBarOverlay(inflater,
284                 "HVAC Driver Temp collapsed",
285                 mNavBarHeight,
286                 Gravity.BOTTOM | Gravity.LEFT);
287 
288         mPassengerTemperatureBarCollapsed = createTemperatureBarOverlay(inflater,
289                 "HVAC Passenger Temp collapsed",
290                 mNavBarHeight,
291                 Gravity.BOTTOM | Gravity.RIGHT);
292 
293         mDriverTemperatureBar = createTemperatureBarOverlay(inflater,
294                 "HVAC Driver Temp",
295                 mTemperatureOverlayHeight,
296                 Gravity.BOTTOM | Gravity.LEFT);
297 
298         mPassengerTemperatureBar = createTemperatureBarOverlay(inflater,
299                 "HVAC Passenger Temp",
300                 mTemperatureOverlayHeight,
301                 Gravity.BOTTOM | Gravity.RIGHT);
302     }
303 
createTemperatureBarOverlay(LayoutInflater inflater, String windowTitle, int windowHeight, int gravity)304     private TemperatureBarOverlay createTemperatureBarOverlay(LayoutInflater inflater,
305             String windowTitle, int windowHeight, int gravity) {
306         WindowManager.LayoutParams params = createTemperatureBarLayoutParams(
307                 windowTitle, windowHeight, gravity);
308         TemperatureBarOverlay button = (TemperatureBarOverlay) inflater
309                 .inflate(R.layout.hvac_temperature_bar_overlay, null);
310         button.setLayoutParams(params);
311         addViewToWindowManagerAndTrack(button, params);
312         return button;
313     }
314 
315     // note the window manager does not copy the layout params but uses the supplied object thus
316     // you need a new copy for each window or change 1 can effect the others
createTemperatureBarLayoutParams(String windowTitle, int windowHeight, int gravity)317     private WindowManager.LayoutParams createTemperatureBarLayoutParams(String windowTitle,
318             int windowHeight, int gravity) {
319         WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
320                 WindowManager.LayoutParams.WRAP_CONTENT,
321                 WindowManager.LayoutParams.WRAP_CONTENT,
322                 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
323                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
324                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
325                 PixelFormat.TRANSLUCENT);
326         lp.x = mTemperatureSideMargin;
327         lp.y = mInitialYOffset;
328         lp.width = mTemperatureOverlayWidth;
329         disableAnimations(lp);
330         lp.setTitle(windowTitle);
331         lp.height = windowHeight;
332         lp.gravity = gravity;
333         return lp;
334     }
335 
336     /**
337      * Disables animations when window manager updates a child view.
338      */
disableAnimations(WindowManager.LayoutParams params)339     private void disableAnimations(WindowManager.LayoutParams params) {
340         try {
341             int currentFlags = (Integer) params.getClass().getField("privateFlags").get(params);
342             params.getClass().getField("privateFlags").set(params, currentFlags | 0x00000040);
343         } catch (Exception e) {
344             Log.e(TAG, "Error disabling animation");
345         }
346     }
347 }
348