Home | History | Annotate | Download | only in hvac
      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