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