Enable using baselayouts without an activity

Add a new CarUi.installBaseLayoutAround() method that
accepts a view and installs the base layout around it.

This is to support PlayDough, which won't have its own
Activties. It can also potentially be used to clean
up the Media implementation of base layouts.

Bug: 168503533
Test: Manually
Change-Id: I3159311392b0138766b33ad3662f7b76b1432d09
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
index a395959..1a8360e 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.ui.core;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.app.Activity;
 import android.content.res.TypedArray;
 import android.os.Build;
@@ -27,6 +29,7 @@
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 
@@ -35,7 +38,6 @@
 import com.android.car.ui.baselayout.InsetsChangedListener;
 import com.android.car.ui.toolbar.ToolbarController;
 import com.android.car.ui.toolbar.ToolbarControllerImpl;
-import com.android.car.ui.utils.CarUiUtils;
 
 import java.util.Map;
 import java.util.WeakHashMap;
@@ -117,6 +119,30 @@
      */
     private void installBaseLayout(Activity activity) {
         boolean toolbarEnabled = getThemeBoolean(activity, R.attr.carUiToolbar);
+        Pair<ToolbarController, InsetsUpdater> results = installBaseLayoutAround(
+                activity,
+                requireViewByRefId(activity.getWindow().getDecorView(), android.R.id.content),
+                toolbarEnabled);
+
+        mToolbarController = results.first;
+        mInsetsUpdater = results.second;
+    }
+
+    /**
+     * Installs a base layout *around* the provided contentView.
+     *
+     * @param activity May be null. Used to dispatch inset changes to, if it implements
+     *                 {@link InsetsChangedListener}
+     * @param contentView The view to install the base layout around.
+     * @param toolbarEnabled If there should be a toolbar in the base layout.
+     * @return Both the {@link ToolbarController} and {@link InsetsUpdater} for the base layout.
+     *         The InsetsUpdater will never be null. The ToolbarController will be null if
+     *         {@code toolbarEnabled} was false.
+     */
+    public static Pair<ToolbarController, InsetsUpdater> installBaseLayoutAround(
+            @Nullable Activity activity,
+            @NonNull View contentView,
+            boolean toolbarEnabled) {
         boolean legacyToolbar = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q;
         @LayoutRes final int baseLayoutRes;
 
@@ -128,34 +154,35 @@
             baseLayoutRes = R.layout.car_ui_base_layout;
         }
 
-        View baseLayout = LayoutInflater.from(activity)
+        View baseLayout = LayoutInflater.from(contentView.getContext())
                 .inflate(baseLayoutRes, null, false);
 
         // Replace the app's content view with a base layout
-        ViewGroup contentView = CarUiUtils.requireViewByRefId(
-                activity.getWindow().getDecorView(), android.R.id.content);
         ViewGroup contentViewParent = (ViewGroup) contentView.getParent();
         int contentIndex = contentViewParent.indexOfChild(contentView);
         contentViewParent.removeView(contentView);
         contentViewParent.addView(baseLayout, contentIndex, contentView.getLayoutParams());
 
         // Add the app's content view to the baseLayout's content view container
-        FrameLayout contentViewContainer = CarUiUtils.requireViewByRefId(baseLayout,
+        FrameLayout contentViewContainer = requireViewByRefId(baseLayout,
                 R.id.car_ui_base_layout_content_container);
         contentViewContainer.addView(contentView, new FrameLayout.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT));
 
+        ToolbarController toolbarController = null;
         if (toolbarEnabled) {
             if (legacyToolbar) {
-                mToolbarController = CarUiUtils.requireViewByRefId(baseLayout, R.id.car_ui_toolbar);
+                toolbarController = requireViewByRefId(baseLayout, R.id.car_ui_toolbar);
             } else {
-                mToolbarController = new ToolbarControllerImpl(baseLayout);
+                toolbarController = new ToolbarControllerImpl(baseLayout);
             }
         }
 
-        mInsetsUpdater = new InsetsUpdater(activity, baseLayout, contentView);
-        mInsetsUpdater.installListeners();
+        InsetsUpdater insetsUpdater = new InsetsUpdater(activity, baseLayout, contentView);
+        insetsUpdater.installListeners();
+
+        return Pair.create(toolbarController, insetsUpdater);
     }
 
     /**
@@ -191,6 +218,7 @@
         private static final String TOP_INSET_TAG = "car_ui_top_inset";
         private static final String BOTTOM_INSET_TAG = "car_ui_bottom_inset";
 
+        @Nullable
         private final Activity mActivity;
         private final View mContentView;
         private final View mContentViewContainer; // Equivalent to mContentView except in Media
@@ -207,14 +235,18 @@
         /**
          * Constructs an InsetsUpdater that calculates and dispatches insets to an {@link Activity}.
          *
-         * @param activity    The activity that is using base layouts
+         * @param activity    The activity that is using base layouts. Used to dispatch insets to if
+         *                    it implements {@link InsetsChangedListener}
          * @param baseLayout  The root view of the base layout
          * @param contentView The android.R.id.content View
          */
-        public InsetsUpdater(Activity activity, View baseLayout, View contentView) {
+        public InsetsUpdater(
+                @Nullable Activity activity,
+                @NonNull View baseLayout,
+                @NonNull View contentView) {
             mActivity = activity;
             mContentView = contentView;
-            mContentViewContainer = CarUiUtils.requireViewByRefId(baseLayout,
+            mContentViewContainer = requireViewByRefId(baseLayout,
                     R.id.car_ui_base_layout_content_container);
 
             mLeftInsetView = baseLayout.findViewWithTag(LEFT_INSET_TAG);
@@ -254,7 +286,7 @@
         public void installListeners() {
             // The global layout listener will run after all the individual layout change listeners
             // so that we only updateInsets once per layout, even if multiple inset views changed
-            mActivity.getWindow().getDecorView().getViewTreeObserver()
+            mContentView.getRootView().getViewTreeObserver()
                     .addOnGlobalLayoutListener(this);
         }
 
@@ -350,8 +382,7 @@
             }
 
             if (!handled) {
-                CarUiUtils.requireViewByRefId(mActivity.getWindow().getDecorView(),
-                        android.R.id.content).setPadding(insets.getLeft(), insets.getTop(),
+                mContentView.setPadding(insets.getLeft(), insets.getTop(),
                         insets.getRight(), insets.getBottom());
             }
         }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/CarUi.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/CarUi.java
index ad67121..ce595d5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/CarUi.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/CarUi.java
@@ -16,15 +16,19 @@
 package com.android.car.ui.core;
 
 import android.app.Activity;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
 
 import com.android.car.ui.baselayout.Insets;
 import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.BaseLayoutController.InsetsUpdater;
 import com.android.car.ui.toolbar.ToolbarController;
 
 import java.lang.reflect.Method;
+import java.util.Objects;
 
 /**
  * Public interface for general CarUi static functions.
@@ -119,6 +123,34 @@
         return result;
     }
 
+    /**
+     * Most apps should not use this method, but instead rely on CarUi automatically
+     * installing the base layout into their activities. See {@link #requireToolbar(Activity)}.
+     *
+     * This method installs the base layout *around* the provided view. As a result, this view
+     * must have a parent ViewGroup.
+     *
+     * When using this method, you can't use the other activity-based methods.
+     * ({@link #requireToolbar(Activity)}, {@link #requireInsets(Activity)}, ect.)
+     *
+     * @param view The view to wrap inside a base layout.
+     * @param hasToolbar if there should be a toolbar in the base layout.
+     * @return The {@link ToolbarController}, which will be null if hasToolbar is false.
+     */
+    @Nullable
+    public static ToolbarController installBaseLayoutAround(
+            View view,
+            InsetsChangedListener insetsChangedListener,
+            boolean hasToolbar) {
+        Pair<ToolbarController, InsetsUpdater> results =
+                BaseLayoutController.installBaseLayoutAround(null, view, hasToolbar);
+
+        Objects.requireNonNull(results.second)
+                .replaceInsetsChangedListenerWith(insetsChangedListener);
+
+        return results.first;
+    }
+
     /* package */ static BaseLayoutController getBaseLayoutController(Activity activity) {
         if (activity.getClassLoader().equals(CarUi.class.getClassLoader())) {
             return BaseLayoutController.getBaseLayout(activity);