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 86 public IBinder onBind(Intent intent) { 87 throw new UnsupportedOperationException("Not yet implemented."); 88 } 89 90 @Override 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 */ 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 221 private void addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params) { 222 mWindowManager.addView(view, params); 223 mAddedViews.add(view); 224 } 225 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 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 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 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 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 */ 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