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 */
16package com.android.car.hvac;
17
18import android.app.Service;
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.content.res.Resources;
24import android.graphics.PixelFormat;
25import android.os.Handler;
26import android.os.IBinder;
27import android.util.DisplayMetrics;
28import android.util.Log;
29import android.view.Gravity;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.WindowManager;
34import android.widget.LinearLayout;
35import com.android.car.hvac.controllers.HvacPanelController;
36import com.android.car.hvac.ui.TemperatureBarOverlay;
37
38/**
39 * Creates a sliding panel for HVAC controls and adds it to the window manager above SystemUI.
40 */
41public class HvacUiService extends Service {
42    private static final String TAG = "HvacUiService";
43
44    private WindowManager mWindowManager;
45
46    private View mPanel;
47    private View mContainer;
48
49    private int mPanelCollapsedHeight;
50    private int mPanelFullExpandedHeight;
51    private int mScreenBottom;
52    private int mScreenWidth;
53
54    private int mTemperatureSideMargin;
55    private int mTemperatureOverlayWidth;
56    private int mTemperatureOverlayHeight;
57    private int mTemperatureBarCollapsedHeight;
58
59    private HvacPanelController mHvacPanelController;
60    private HvacController mHvacController;
61
62    private ViewGroup mDriverTemperatureBarTouchOverlay;
63    private ViewGroup mPassengerTemperatureBarTouchOverlay;
64    private TemperatureBarOverlay mDriverTemperatureBar;
65    private TemperatureBarOverlay mPassengerTemperatureBar;
66
67    @Override
68    public IBinder onBind(Intent intent) {
69        throw new UnsupportedOperationException("Not yet implemented.");
70    }
71
72    @Override
73    public void onCreate() {
74        Resources res = getResources();
75        mPanelCollapsedHeight = res.getDimensionPixelSize(R.dimen.car_hvac_panel_collapsed_height);
76        mPanelFullExpandedHeight
77                = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);
78
79        mTemperatureSideMargin = res.getDimensionPixelSize(R.dimen.temperature_side_margin);
80        mTemperatureOverlayWidth = res.getDimensionPixelSize(R.dimen.temperature_bar_width_expanded);
81        mTemperatureOverlayHeight
82                = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);
83        mTemperatureBarCollapsedHeight
84                = res.getDimensionPixelSize(R.dimen.temperature_bar_collapsed_height);
85
86        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
87        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
88
89        DisplayMetrics metrics = new DisplayMetrics();
90        mWindowManager.getDefaultDisplay().getRealMetrics(metrics);
91        mScreenBottom = metrics.heightPixels - getStatusBarHeight();
92        mScreenWidth = metrics.widthPixels;
93
94        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
95                WindowManager.LayoutParams.WRAP_CONTENT,
96                WindowManager.LayoutParams.WRAP_CONTENT,
97                WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
98                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
99                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
100                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
101                PixelFormat.TRANSLUCENT);
102
103        params.packageName = this.getPackageName();
104        params.gravity = Gravity.TOP | Gravity.LEFT;
105
106        params.x = 0;
107        params.y = 0;
108
109        params.width = mScreenWidth;
110        params.height = mScreenBottom;
111        disableAnimations(params);
112
113        mContainer = inflater.inflate(R.layout.hvac_panel, null);
114        mContainer.setLayoutParams(params);
115
116        // The top padding should be calculated on the screen height and the height of the
117        // expanded hvac panel. The space defined by the padding is meant to be clickable for
118        // dismissing the hvac panel.
119        int topPadding = mScreenBottom - mPanelFullExpandedHeight;
120        mContainer.setPadding(0, topPadding, 0, 0);
121
122        mContainer.setFocusable(false);
123        mContainer.setClickable(false);
124        mContainer.setFocusableInTouchMode(false);
125
126        mPanel = mContainer.findViewById(R.id.hvac_center_panel);
127        mPanel.getLayoutParams().height = mPanelCollapsedHeight;
128
129        mWindowManager.addView(mContainer, params);
130
131        createTemperatureBars(inflater);
132        mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
133                mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
134                mDriverTemperatureBarTouchOverlay, mPassengerTemperatureBarTouchOverlay);
135        Intent bindIntent = new Intent(this /* context */, HvacController.class);
136        if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
137            Log.e(TAG, "Failed to connect to HvacController.");
138        }
139    }
140
141    @Override
142    public void onDestroy() {
143        mWindowManager.removeView(mPanel);
144        if(mHvacController != null){
145            unbindService(mServiceConnection);
146        }
147    }
148
149    private ServiceConnection mServiceConnection = new ServiceConnection() {
150        @Override
151        public void onServiceConnected(ComponentName className, IBinder service) {
152            mHvacController = ((HvacController.LocalBinder) service).getService();
153            final Context context = HvacUiService.this;
154
155            final Runnable r = new Runnable() {
156                @Override
157                public void run() {
158                    // Once the hvac controller has refreshed its values from the vehicle,
159                    // bind all the values.
160                    mHvacPanelController.updateHvacController(mHvacController);
161                }
162            };
163
164            if (mHvacController != null) {
165                mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
166            }
167        }
168
169        @Override
170        public void onServiceDisconnected(ComponentName className) {
171            mHvacController = null;
172            mHvacPanelController.updateHvacController(null);
173            //TODO: b/29126575 reconnect to controller if it is restarted
174        }
175    };
176
177    private WindowManager.LayoutParams createClickableOverlayLayoutParam() {
178        return new WindowManager.LayoutParams(
179                WindowManager.LayoutParams.WRAP_CONTENT,
180                WindowManager.LayoutParams.WRAP_CONTENT,
181                WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
182                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
183                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
184                PixelFormat.TRANSLUCENT);
185    }
186
187    private TemperatureBarOverlay createTemperatureBarOverlay(LayoutInflater inflater,
188            int gravity) {
189        TemperatureBarOverlay button = (TemperatureBarOverlay) inflater
190                .inflate(R.layout.hvac_temperature_bar_overlay, null);
191
192        WindowManager.LayoutParams params = createClickableOverlayLayoutParam();
193        params.gravity = gravity;
194        params.x = mTemperatureSideMargin;
195        params.y = mScreenBottom - mTemperatureOverlayHeight;
196        params.width = mTemperatureOverlayWidth;
197        params.height = mTemperatureOverlayHeight;
198
199        disableAnimations(params);
200        button.setLayoutParams(params);
201        mWindowManager.addView(button, params);
202
203        return button;
204    }
205
206    /**
207     * Creates a touchable overlay in the dimensions of a collapsed {@link TemperatureBarOverlay}.
208     * @return a {@link ViewGroup} that was added to the {@link WindowManager}
209     */
210    private ViewGroup addTemperatureTouchOverlay(int gravity) {
211        WindowManager.LayoutParams params = createClickableOverlayLayoutParam();
212        params.gravity = gravity;
213        params.x = mTemperatureSideMargin;
214        params.y = mScreenBottom - mTemperatureBarCollapsedHeight;
215        params.width = mTemperatureOverlayWidth;
216        params.height = mTemperatureBarCollapsedHeight;
217
218        ViewGroup overlay = new LinearLayout(this /* context */);
219        overlay.setLayoutParams(params);
220        mWindowManager.addView(overlay, params);
221        return overlay;
222    }
223
224    private void createTemperatureBars(LayoutInflater inflater) {
225        mDriverTemperatureBar
226                = createTemperatureBarOverlay(inflater, Gravity.TOP | Gravity.LEFT);
227        mPassengerTemperatureBar
228                = createTemperatureBarOverlay(inflater, Gravity.TOP | Gravity.RIGHT);
229
230        // Create a transparent overlay that is the size of the collapsed temperature bar.
231        // It will receive touch events and trigger the expand/collapse of the panel. This is
232        // necessary since changing the height of the temperature bar overlay dynamically, causes
233        // a jank when WindowManager updates the view with a new height. This hack allows us
234        // to maintain the temperature bar overlay at constant (expanded) height and just
235        // update whether or not it is touchable/clickable.
236        mDriverTemperatureBarTouchOverlay
237                = addTemperatureTouchOverlay(Gravity.TOP | Gravity.LEFT);
238        mPassengerTemperatureBarTouchOverlay
239                = addTemperatureTouchOverlay(Gravity.TOP | Gravity.RIGHT);
240    }
241
242    /**
243     * Disables animations when window manager updates a child view.
244     */
245    private void disableAnimations(WindowManager.LayoutParams params) {
246        try {
247            int currentFlags = (Integer) params.getClass().getField("privateFlags").get(params);
248            params.getClass().getField("privateFlags").set(params, currentFlags | 0x00000040);
249        } catch (Exception e) {
250            Log.e(TAG, "Error disabling animation");
251        }
252    }
253
254    private int getStatusBarHeight() {
255        int result = 0;
256        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
257        if (resourceId > 0) {
258            result = getResources().getDimensionPixelSize(resourceId);
259        }
260        return result;
261    }
262}
263