1 /*
2  * Copyright (C) 2023 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.chassis.car.ui.plugin.toolbar;
18 
19 import android.content.Context;
20 import android.view.LayoutInflater;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.view.ViewTreeObserver;
24 import android.widget.FrameLayout;
25 
26 import androidx.annotation.LayoutRes;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 
30 import com.android.car.ui.plugin.oemapis.Consumer;
31 import com.android.car.ui.plugin.oemapis.FocusAreaOEMV1;
32 import com.android.car.ui.plugin.oemapis.FocusParkingViewOEMV1;
33 import com.android.car.ui.plugin.oemapis.Function;
34 import com.android.car.ui.plugin.oemapis.InsetsOEMV1;
35 import com.android.car.ui.plugin.oemapis.toolbar.ToolbarControllerOEMV1;
36 import com.android.car.ui.plugin.oemapis.toolbar.ToolbarControllerOEMV2;
37 import com.android.car.ui.plugin.oemapis.toolbar.ToolbarControllerOEMV3;
38 
39 import com.chassis.car.ui.plugin.R;
40 
41 /**
42  * A helper class for implementing installBaseLayoutAround from {@code PluginFactory}
43  */
44 public class BaseLayoutInstaller {
45 
46     private static int sBaseLayoutId = 0;
47 
48     /**
49      * Installs the base layout around the contentView. Optionally installs a toolbar and returns
50      * an implementation of {@code ToolbarControllerOEMV1} if the toolbar is enabled.
51      */
52     @Nullable
installBaseLayoutAroundV1( @onNull Context pluginContext, @NonNull View contentView, @Nullable java.util.function.Consumer<InsetsOEMV1> insetsChangedListener, boolean toolbarEnabled, boolean fullscreen, @Nullable java.util.function.Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory, @Nullable java.util.function.Function<Context, FocusAreaOEMV1> focusAreaFactory)53     public static ToolbarControllerOEMV1 installBaseLayoutAroundV1(
54             @NonNull Context pluginContext,
55             @NonNull View contentView,
56             @Nullable java.util.function.Consumer<InsetsOEMV1> insetsChangedListener,
57             boolean toolbarEnabled,
58             boolean fullscreen,
59             @Nullable java.util.function.Function<Context, FocusParkingViewOEMV1>
60                     focusParkingViewFactory,
61             @Nullable java.util.function.Function<Context, FocusAreaOEMV1> focusAreaFactory) {
62         ToolbarControllerImpl toolbarControllerImpl = installBaseLayoutAround(
63                 pluginContext, contentView, insetsChangedListener != null
64                         ? (Consumer<InsetsOEMV1>) insets -> insetsChangedListener.accept(insets)
65                         : null, toolbarEnabled, fullscreen,
66                 focusParkingViewFactory != null ? ((Function<Context, FocusParkingViewOEMV1>)
67                 context -> focusParkingViewFactory.apply(context)) : null,
68                 focusAreaFactory != null ? ((Function<Context, FocusAreaOEMV1>)
69                 context -> focusAreaFactory.apply(context)) : null);
70         return !toolbarEnabled ? null : new ToolbarAdapterProxyV1(pluginContext,
71                 toolbarControllerImpl);
72     }
73 
74     /**
75      * Installs the base layout around the contentView. Optionally installs a toolbar and returns
76      * an implementation of {@code ToolbarControllerOEMV2} if the toolbar is enabled.
77      */
78     @Nullable
installBaseLayoutAroundV2( @onNull Context pluginContext, @NonNull View contentView, @Nullable Consumer<InsetsOEMV1> insetsChangedListener, boolean toolbarEnabled, boolean fullscreen, @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory, @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory)79     public static ToolbarControllerOEMV2 installBaseLayoutAroundV2(
80             @NonNull Context pluginContext,
81             @NonNull View contentView,
82             @Nullable Consumer<InsetsOEMV1> insetsChangedListener,
83             boolean toolbarEnabled,
84             boolean fullscreen,
85             @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory,
86             @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory) {
87         ToolbarControllerImpl toolbarControllerImpl = installBaseLayoutAround(
88                 pluginContext, contentView, insetsChangedListener, toolbarEnabled, fullscreen,
89                 focusParkingViewFactory, focusAreaFactory);
90         return !toolbarEnabled ? null : new ToolbarAdapterProxyV2(pluginContext,
91                 toolbarControllerImpl);
92     }
93 
94     /**
95      * Installs the base layout around the contentView. Optionally installs a toolbar and returns
96      * an implementation of {@code ToolbarControllerOEMV3} if the toolbar is enabled.
97      */
98     @Nullable
installBaseLayoutAroundV3( @onNull Context pluginContext, @NonNull View contentView, @Nullable Consumer<InsetsOEMV1> insetsChangedListener, boolean toolbarEnabled, boolean fullscreen, @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory, @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory)99     public static ToolbarControllerOEMV3 installBaseLayoutAroundV3(
100             @NonNull Context pluginContext,
101             @NonNull View contentView,
102             @Nullable Consumer<InsetsOEMV1> insetsChangedListener,
103             boolean toolbarEnabled,
104             boolean fullscreen,
105             @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory,
106             @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory) {
107         ToolbarControllerImpl toolbarControllerImpl = installBaseLayoutAround(
108                 pluginContext, contentView, insetsChangedListener, toolbarEnabled, fullscreen,
109                 focusParkingViewFactory, focusAreaFactory);
110         return !toolbarEnabled ? null : new ToolbarAdapterProxyV3(pluginContext,
111                 toolbarControllerImpl);
112     }
113 
114     /**
115      * Implementation of installBaseLayoutAround from {@code PluginFactory}
116      */
installBaseLayoutAround( @onNull Context pluginContext, @NonNull View contentView, @Nullable Consumer<InsetsOEMV1> insetsChangedListener, boolean toolbarEnabled, boolean fullScreen, @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory, @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory)117     private static ToolbarControllerImpl installBaseLayoutAround(
118             @NonNull Context pluginContext,
119             @NonNull View contentView,
120             @Nullable Consumer<InsetsOEMV1> insetsChangedListener,
121             boolean toolbarEnabled,
122             boolean fullScreen,
123             @Nullable Function<Context, FocusParkingViewOEMV1> focusParkingViewFactory,
124             @Nullable Function<Context, FocusAreaOEMV1> focusAreaFactory) {
125 
126         @LayoutRes int layout = toolbarEnabled
127                 ? R.layout.car_ui_portrait_base_layout_toolbar
128                 : R.layout.car_ui_base_layout;
129 
130         sBaseLayoutId = R.id.car_ui_base_layout_content_container;
131         ViewGroup baseLayout = (ViewGroup) LayoutInflater.from(pluginContext).inflate(
132                 layout, null, false);
133 
134         // Replace the app's content view with a base layout
135         ViewGroup contentViewParent = (ViewGroup) contentView.getParent();
136         int contentIndex = contentViewParent.indexOfChild(contentView);
137         contentViewParent.removeView(contentView);
138         contentViewParent.addView(baseLayout, contentIndex, contentView.getLayoutParams());
139 
140         // Add the app's content view to the baseLayout's content view container
141         FrameLayout contentViewContainer = baseLayout.requireViewById(
142                         R.id.car_ui_base_layout_content_container);
143         contentViewContainer.addView(contentView, new FrameLayout.LayoutParams(
144                 ViewGroup.LayoutParams.MATCH_PARENT,
145                 ViewGroup.LayoutParams.MATCH_PARENT));
146 
147         ToolbarControllerImpl toolbarController = null;
148         if (toolbarEnabled) {
149             toolbarController = new ToolbarControllerImpl(baseLayout, pluginContext);
150         }
151 
152         InsetsUpdater updater = new InsetsUpdater(baseLayout, contentView);
153         updater.replaceInsetsChangedListenerWith(insetsChangedListener);
154         updater.installListeners();
155 
156         return toolbarController;
157     }
158 
159     /**
160      * InsetsUpdater waits for layout changes, and when there is one, calculates the appropriate
161      * insets into the content view.
162      */
163     private static final class InsetsUpdater implements ViewTreeObserver.OnGlobalLayoutListener {
164         // These tags mark views that should overlay the content view in the base layout.
165         // Apps will then be able to draw under these views, but will be encouraged to not put
166         // any user-interactable content there.
167         private static final String LEFT_INSET_TAG = "car_ui_portrait_left_inset";
168         private static final String RIGHT_INSET_TAG = "car_ui_portrait_right_inset";
169         private static final String TOP_INSET_TAG = "car_ui_portrait_top_inset";
170         private static final String BOTTOM_INSET_TAG = "car_ui_portrait_bottom_inset";
171 
172         private final View mContentView;
173         private final View mContentViewContainer; // Equivalent to mContentView except in Media
174         private final View mLeftInsetView;
175         private final View mRightInsetView;
176         private final View mTopInsetView;
177         private final View mBottomInsetView;
178         private Consumer<InsetsOEMV1> mInsetsChangedListenerDelegate;
179 
180         private boolean mInsetsDirty = true;
181         private InsetsOEMV1 mInsets = new InsetsOEMV1();
182 
183         /**
184          * Constructs an InsetsUpdater that calculates and dispatches insets to the method provided
185          * via {@link #replaceInsetsChangedListenerWith(Consumer)}.
186          *
187          * @param baseLayout  The root view of the base layout
188          * @param contentView The android.R.id.content View
189          */
InsetsUpdater( View baseLayout, View contentView)190         InsetsUpdater(
191                 View baseLayout,
192                 View contentView) {
193             mContentView = contentView;
194             mContentViewContainer = baseLayout.requireViewById(sBaseLayoutId);
195 
196             mLeftInsetView = baseLayout.findViewWithTag(LEFT_INSET_TAG);
197             mRightInsetView = baseLayout.findViewWithTag(RIGHT_INSET_TAG);
198             mTopInsetView = baseLayout.findViewWithTag(TOP_INSET_TAG);
199             mBottomInsetView = baseLayout.findViewWithTag(BOTTOM_INSET_TAG);
200 
201             final View.OnLayoutChangeListener layoutChangeListener =
202                     (View v, int left, int top, int right, int bottom,
203                             int oldLeft, int oldTop, int oldRight, int oldBottom) -> {
204                         if (left != oldLeft || top != oldTop
205                                 || right != oldRight || bottom != oldBottom) {
206                             mInsetsDirty = true;
207                         }
208                     };
209 
210             if (mLeftInsetView != null) {
211                 mLeftInsetView.addOnLayoutChangeListener(layoutChangeListener);
212             }
213             if (mRightInsetView != null) {
214                 mRightInsetView.addOnLayoutChangeListener(layoutChangeListener);
215             }
216             if (mTopInsetView != null) {
217                 mTopInsetView.addOnLayoutChangeListener(layoutChangeListener);
218             }
219             if (mBottomInsetView != null) {
220                 mBottomInsetView.addOnLayoutChangeListener(layoutChangeListener);
221             }
222             contentView.addOnLayoutChangeListener(layoutChangeListener);
223             mContentViewContainer.addOnLayoutChangeListener(layoutChangeListener);
224         }
225 
226         /**
227          * Install a global layout listener, during which the insets will be recalculated and
228          * dispatched.
229          */
installListeners()230         public void installListeners() {
231             // The global layout listener will run after all the individual layout change listeners
232             // so that we only updateInsets once per layout, even if multiple inset views changed
233             mContentView.getRootView().getViewTreeObserver()
234                     .addOnGlobalLayoutListener(this);
235         }
236 
getInsets()237         InsetsOEMV1 getInsets() {
238             return mInsets;
239         }
240 
241         // TODO remove this method / cleanup this class
replaceInsetsChangedListenerWith(Consumer<InsetsOEMV1> listener)242         public void replaceInsetsChangedListenerWith(Consumer<InsetsOEMV1> listener) {
243             mInsetsChangedListenerDelegate = listener;
244         }
245 
246         /**
247          * onGlobalLayout() should recalculate the amount of insets we need, and then dispatch them.
248          */
249         @Override
onGlobalLayout()250         public void onGlobalLayout() {
251             if (!mInsetsDirty) {
252                 return;
253             }
254 
255             // Calculate how much each inset view overlays the content view
256 
257             // These initial values are for Media Center's implementation of base layouts.
258             // They should evaluate to 0 in all other apps, because the content view and content
259             // view container have the same size and position there.
260             int top = Math.max(0,
261                     getTopOfView(mContentViewContainer) - getTopOfView(mContentView));
262             int left = Math.max(0,
263                     getLeftOfView(mContentViewContainer) - getLeftOfView(mContentView));
264             int right = Math.max(0,
265                     getRightOfView(mContentView) - getRightOfView(mContentViewContainer));
266             int bottom = Math.max(0,
267                     getBottomOfView(mContentView) - getBottomOfView(mContentViewContainer));
268             if (mTopInsetView != null) {
269                 top += Math.max(0,
270                         getBottomOfView(mTopInsetView) - getTopOfView(mContentViewContainer));
271             }
272             if (mBottomInsetView != null) {
273                 bottom += Math.max(0,
274                         getBottomOfView(mContentViewContainer) - getTopOfView(mBottomInsetView));
275             }
276             if (mLeftInsetView != null) {
277                 left += Math.max(0,
278                         getRightOfView(mLeftInsetView) - getLeftOfView(mContentViewContainer));
279             }
280             if (mRightInsetView != null) {
281                 right += Math.max(0,
282                         getRightOfView(mContentViewContainer) - getLeftOfView(mRightInsetView));
283             }
284             InsetsOEMV1 insets = new InsetsOEMV1(left, top, right, bottom);
285 
286             mInsetsDirty = false;
287             if (!insets.equals(mInsets)) {
288                 mInsets = insets;
289                 if (mInsetsChangedListenerDelegate != null) {
290                     mInsetsChangedListenerDelegate.accept(insets);
291                 }
292             }
293         }
294 
getLeftOfView(View v)295         private static int getLeftOfView(View v) {
296             int[] position = new int[2];
297             v.getLocationOnScreen(position);
298             return position[0];
299         }
300 
getRightOfView(View v)301         private static int getRightOfView(View v) {
302             int[] position = new int[2];
303             v.getLocationOnScreen(position);
304             return position[0] + v.getWidth();
305         }
306 
getTopOfView(View v)307         private static int getTopOfView(View v) {
308             int[] position = new int[2];
309             v.getLocationOnScreen(position);
310             return position[1];
311         }
312 
getBottomOfView(View v)313         private static int getBottomOfView(View v) {
314             int[] position = new int[2];
315             v.getLocationOnScreen(position);
316             return position[1] + v.getHeight();
317         }
318     }
319 }
320