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