1 /*
2  * Copyright (C) 2020 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 
17 package com.android.systemui.car.window;
18 
19 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
20 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
21 
22 import android.content.Context;
23 import android.graphics.PixelFormat;
24 import android.os.Binder;
25 import android.view.DisplayCutout;
26 import android.view.Gravity;
27 import android.view.LayoutInflater;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.WindowInsets;
32 import android.view.WindowManager;
33 import android.widget.LinearLayout;
34 
35 import com.android.systemui.R;
36 import com.android.systemui.dagger.SysUISingleton;
37 import com.android.systemui.statusbar.policy.ConfigurationController;
38 
39 import javax.inject.Inject;
40 
41 /**
42  * Controls the expansion state of the primary window which will contain all of the fullscreen sysui
43  * behavior. This window still has a collapsed state in order to watch for swipe events to expand
44  * this window for the notification panel.
45  */
46 @SysUISingleton
47 public class SystemUIOverlayWindowController implements
48         ConfigurationController.ConfigurationListener {
49 
50     /**
51      * Touch listener to get touches on the view.
52      */
53     public interface OnTouchListener {
54 
55         /**
56          * Called when a touch happens on the view.
57          */
onTouch(View v, MotionEvent event)58         void onTouch(View v, MotionEvent event);
59     }
60 
61     private final Context mContext;
62     private final WindowManager mWindowManager;
63 
64     private ViewGroup mBaseLayout;
65     private WindowManager.LayoutParams mLp;
66     private WindowManager.LayoutParams mLpChanged;
67     private boolean mIsAttached = false;
68     private boolean mVisible = false;
69     private boolean mFocusable = false;
70     private boolean mUsingStableInsets = false;
71 
72     @Inject
SystemUIOverlayWindowController( Context context, WindowManager windowManager, ConfigurationController configurationController)73     public SystemUIOverlayWindowController(
74             Context context,
75             WindowManager windowManager,
76             ConfigurationController configurationController) {
77         mContext = context;
78         mWindowManager = windowManager;
79 
80         mLpChanged = new WindowManager.LayoutParams();
81         mBaseLayout = (ViewGroup) LayoutInflater.from(context)
82                 .inflate(R.layout.sysui_overlay_window, /* root= */ null, false);
83         configurationController.addCallback(this);
84     }
85 
86     /**
87      * Register to {@link OnTouchListener}
88      */
registerOutsideTouchListener(OnTouchListener listener)89     public void registerOutsideTouchListener(OnTouchListener listener) {
90         mBaseLayout.setOnTouchListener(new View.OnTouchListener() {
91             @Override
92             public boolean onTouch(View v, MotionEvent event) {
93                 listener.onTouch(v, event);
94                 return false;
95             }
96         });
97     }
98 
99     /** Returns the base view of the primary window. */
getBaseLayout()100     public ViewGroup getBaseLayout() {
101         return mBaseLayout;
102     }
103 
104     /** Returns {@code true} if the window is already attached. */
isAttached()105     public boolean isAttached() {
106         return mIsAttached;
107     }
108 
109     /** Attaches the window to the window manager. */
attach()110     public void attach() {
111         if (mIsAttached) {
112             return;
113         }
114         mIsAttached = true;
115         // Now that the status bar window encompasses the sliding panel and its
116         // translucent backdrop, the entire thing is made TRANSLUCENT and is
117         // hardware-accelerated.
118         mLp = new WindowManager.LayoutParams(
119                 ViewGroup.LayoutParams.MATCH_PARENT,
120                 ViewGroup.LayoutParams.MATCH_PARENT,
121                 WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE,
122                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
123                         | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
124                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
125                         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
126                         | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
127                         | WindowManager.LayoutParams.FLAG_DIM_BEHIND,
128                 PixelFormat.TRANSLUCENT);
129         mLp.token = new Binder();
130         mLp.gravity = Gravity.TOP;
131         mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
132         mLp.dimAmount = 0f;
133         mLp.setTitle("SystemUIOverlayWindow");
134         mLp.packageName = mContext.getPackageName();
135         mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
136         mLp.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
137 
138         mWindowManager.addView(mBaseLayout, mLp);
139         mLpChanged.copyFrom(mLp);
140         setWindowVisible(false);
141     }
142 
143     /** Sets the types of insets to fit. Note: This should be rarely used. */
setFitInsetsTypes(@indowInsets.Type.InsetsType int types)144     public void setFitInsetsTypes(@WindowInsets.Type.InsetsType int types) {
145         mLpChanged.setFitInsetsTypes(types);
146         mLpChanged.setFitInsetsIgnoringVisibility(mUsingStableInsets);
147         updateWindow();
148     }
149 
150     /** Sets the sides of system bar insets to fit. Note: This should be rarely used. */
setFitInsetsSides(@indowInsets.Side.InsetsSide int sides)151     public void setFitInsetsSides(@WindowInsets.Side.InsetsSide int sides) {
152         mLpChanged.setFitInsetsSides(sides);
153         mLpChanged.setFitInsetsIgnoringVisibility(mUsingStableInsets);
154         updateWindow();
155     }
156 
157     /** Sets the window to the visible state. */
setWindowVisible(boolean visible)158     public void setWindowVisible(boolean visible) {
159         mVisible = visible;
160         if (visible) {
161             mBaseLayout.setVisibility(View.VISIBLE);
162         } else {
163             mBaseLayout.setVisibility(View.INVISIBLE);
164         }
165         updateWindow();
166     }
167 
168     /** Sets the window to be focusable. */
setWindowFocusable(boolean focusable)169     public void setWindowFocusable(boolean focusable) {
170         mFocusable = focusable;
171         if (focusable) {
172             mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
173         } else {
174             mLpChanged.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
175         }
176         updateWindow();
177     }
178 
179     /** Sets the dim behind the window */
setDimBehind(float dimAmount)180     public void setDimBehind(float dimAmount) {
181         mLpChanged.dimAmount = dimAmount;
182         updateWindow();
183     }
184 
185     /** Sets the window to enable IME. */
setWindowNeedsInput(boolean needsInput)186     public void setWindowNeedsInput(boolean needsInput) {
187         if (needsInput) {
188             mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
189         } else {
190             mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
191         }
192         updateWindow();
193     }
194 
195     /** Returns {@code true} if the window is visible */
isWindowVisible()196     public boolean isWindowVisible() {
197         return mVisible;
198     }
199 
isWindowFocusable()200     public boolean isWindowFocusable() {
201         return mFocusable;
202     }
203 
setUsingStableInsets(boolean useStableInsets)204     protected void setUsingStableInsets(boolean useStableInsets) {
205         mUsingStableInsets = useStableInsets;
206     }
207 
updateWindow()208     private void updateWindow() {
209         if (mLp != null && mLp.copyFrom(mLpChanged) != 0) {
210             if (isAttached()) {
211                 handleDisplayCutout();
212                 mLp.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
213                 mWindowManager.updateViewLayout(mBaseLayout, mLp);
214             }
215         }
216     }
217 
218     /**
219      * Certain device configurations may contain a curvature at the top-left or top-right of the
220      * screen. In those cases, overlay window should still render under the cutout in order to cover
221      * the entire screen, but the content of HVAC and Notification Center should be centered around
222      * the app area.
223      *
224      * TODO (b/326495987): Find an alternative approach to update all component together rather than
225      * only measuring and updating HVAC and notifications.
226      */
handleDisplayCutout()227     private void handleDisplayCutout() {
228         DisplayCutout cutout =
229                 mWindowManager.getCurrentWindowMetrics().getWindowInsets().getDisplayCutout();
230         if (cutout != null) {
231             int leftMargin = cutout.getBoundingRectLeft().width();
232             int rightMargin = cutout.getBoundingRectRight().width();
233             int appWindowWidth = mBaseLayout.getWidth() - (leftMargin + rightMargin);
234 
235             View notificationsPanelView = mBaseLayout.findViewById(R.id.notifications);
236             if (notificationsPanelView != null) {
237                 ViewGroup.MarginLayoutParams newLayoutParams =
238                         new ViewGroup.MarginLayoutParams(notificationsPanelView.getLayoutParams());
239                 newLayoutParams.width = appWindowWidth;
240                 newLayoutParams.leftMargin = leftMargin;
241                 newLayoutParams.rightMargin = rightMargin;
242                 notificationsPanelView.setLayoutParams(newLayoutParams);
243             }
244 
245             View hvacPanelView = mBaseLayout.findViewById(R.id.hvac_panel);
246             if (hvacPanelView != null) {
247                 LinearLayout.LayoutParams newLayoutParams =
248                         (LinearLayout.LayoutParams) hvacPanelView.getLayoutParams();
249                 newLayoutParams.width = appWindowWidth;
250                 newLayoutParams.leftMargin = leftMargin;
251                 newLayoutParams.rightMargin = rightMargin;
252                 hvacPanelView.setLayoutParams(newLayoutParams);
253             }
254         }
255     }
256 }
257