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 package com.android.car.ui.core;
17 
18 import static com.android.car.ui.core.CarUi.MIN_TARGET_API;
19 import static com.android.car.ui.utils.CarUiUtils.getThemeBoolean;
20 import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
21 
22 import android.annotation.TargetApi;
23 import android.app.Activity;
24 import android.view.View;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.fragment.app.Fragment;
29 import androidx.fragment.app.FragmentActivity;
30 
31 import com.android.car.ui.R;
32 import com.android.car.ui.baselayout.Insets;
33 import com.android.car.ui.baselayout.InsetsChangedListener;
34 import com.android.car.ui.pluginsupport.PluginFactorySingleton;
35 import com.android.car.ui.toolbar.ToolbarController;
36 
37 import java.util.Map;
38 import java.util.WeakHashMap;
39 
40 /**
41  * BaseLayoutController accepts an {@link Activity} and sets up the base layout inside of it.
42  * It also exposes a {@link ToolbarController} to access the toolbar. This may be null if
43  * used with a base layout without a Toolbar.
44  */
45 @TargetApi(MIN_TARGET_API)
46 public final class BaseLayoutController {
47 
48     private static final Map<Activity, BaseLayoutController> sBaseLayoutMap = new WeakHashMap<>();
49 
50     private InsetsUpdater mInsetsUpdater;
51 
52     /**
53      * Gets a BaseLayoutController for the given {@link Activity}. Must have called
54      * {@link #build(Activity)} with the same activity earlier, otherwise will return null.
55      */
56     @Nullable
getBaseLayoutController(@ullable Activity activity)57     /* package */ static BaseLayoutController getBaseLayoutController(@Nullable Activity activity) {
58         return sBaseLayoutMap.get(activity);
59     }
60 
61     @Nullable
62     private ToolbarController mToolbarController;
63 
BaseLayoutController(Activity activity)64     private BaseLayoutController(Activity activity) {
65         installBaseLayout(activity);
66     }
67 
68     /**
69      * Create a new BaseLayoutController for the given {@link Activity}.
70      *
71      * <p>You can get a reference to it by calling {@link #getBaseLayoutController(Activity)}.
72      */
73     /* package */
build(Activity activity)74     static void build(Activity activity) {
75         if (getThemeBoolean(activity, R.attr.carUiBaseLayout)) {
76             sBaseLayoutMap.put(activity, new BaseLayoutController(activity));
77         }
78     }
79 
80     /**
81      * Destroy the BaseLayoutController for the given {@link Activity}.
82      */
83     /* package */
destroy(Activity activity)84     static void destroy(Activity activity) {
85         sBaseLayoutMap.remove(activity);
86     }
87 
88     /**
89      * Gets the {@link ToolbarController} for activities created with carUiBaseLayout and
90      * carUiToolbar set to true.
91      */
92     @Nullable
getToolbarController()93     /* package */ ToolbarController getToolbarController() {
94         return mToolbarController;
95     }
96 
getInsets()97     /* package */ Insets getInsets() {
98         return mInsetsUpdater.getInsets();
99     }
100 
dispatchNewInsets(Insets insets)101     /* package */ void dispatchNewInsets(Insets insets) {
102         mInsetsUpdater.onCarUiInsetsChanged(insets);
103     }
104 
replaceInsetsChangedListenerWith(InsetsChangedListener listener)105     /* package */ void replaceInsetsChangedListenerWith(InsetsChangedListener listener) {
106         mInsetsUpdater.replaceInsetsChangedListenerWith(listener);
107     }
108 
109     /**
110      * Installs the base layout into an activity, moving its content view under the base layout.
111      *
112      * <p>This function must be called during the onCreate() of the {@link Activity}.
113      *
114      * @param activity The {@link Activity} to install a base layout in.
115      */
installBaseLayout(Activity activity)116     private void installBaseLayout(Activity activity) {
117         boolean toolbarEnabled = getThemeBoolean(activity, R.attr.carUiToolbar);
118 
119         View contentView =
120                 requireViewByRefId(activity.getWindow().getDecorView(), android.R.id.content);
121 
122         mInsetsUpdater = new InsetsUpdater(activity, contentView);
123         mToolbarController = PluginFactorySingleton.get(activity)
124                 .installBaseLayoutAround(
125                         contentView,
126                         mInsetsUpdater,
127                         toolbarEnabled,
128                         true);
129     }
130 
131     /**
132      * InsetsUpdater waits for layout changes, and when there is one, calculates the appropriate
133      * insets into the content view.
134      *
135      * <p>It then calls {@link InsetsChangedListener#onCarUiInsetsChanged(Insets)} on the
136      * {@link Activity} and any {@link Fragment Fragments} the Activity might have. If
137      * none of the Activity/Fragments implement {@link InsetsChangedListener}, it will set
138      * padding on the content view equal to the insets.
139      */
140     public static final class InsetsUpdater implements InsetsChangedListener {
141         private InsetsChangedListener mInsetsChangedListenerDelegate;
142 
143         @Nullable
144         private Activity mActivity;
145         private View mContentView;
146 
147         @NonNull
148         private Insets mInsets = new Insets();
149 
150         /**
151          * Constructs an InsetsUpdater that calculates and dispatches insets to an {@link Activity}.
152          *
153          * @param activity    The activity that is using base layouts. Used to dispatch insets to if
154          *                    it implements {@link InsetsChangedListener}
155          * @param contentView The android.R.id.content View
156          */
InsetsUpdater( @ullable Activity activity, @NonNull View contentView)157         InsetsUpdater(
158                 @Nullable Activity activity,
159                 @NonNull View contentView) {
160             mActivity = activity;
161             mContentView = contentView;
162         }
163 
164         @NonNull
getInsets()165         Insets getInsets() {
166             return mInsets;
167         }
168 
replaceInsetsChangedListenerWith(InsetsChangedListener listener)169         public void replaceInsetsChangedListenerWith(InsetsChangedListener listener) {
170             mInsetsChangedListenerDelegate = listener;
171         }
172 
173         @Override
onCarUiInsetsChanged(@onNull Insets insets)174         public void onCarUiInsetsChanged(@NonNull Insets insets) {
175             if (mInsets.equals(insets)) {
176                 return;
177             }
178 
179             mInsets = insets;
180 
181             boolean handled = false;
182             if (mInsetsChangedListenerDelegate != null) {
183                 mInsetsChangedListenerDelegate.onCarUiInsetsChanged(insets);
184                 handled = true;
185             } else {
186                 // If an explicit InsetsChangedListener is not provided,
187                 // pass the insets to activities and fragments
188                 if (mActivity instanceof InsetsChangedListener) {
189                     ((InsetsChangedListener) mActivity).onCarUiInsetsChanged(insets);
190                     handled = true;
191                 }
192 
193                 if (mActivity instanceof FragmentActivity) {
194                     for (Fragment fragment : ((FragmentActivity) mActivity)
195                             .getSupportFragmentManager().getFragments()) {
196                         if (fragment instanceof InsetsChangedListener) {
197                             ((InsetsChangedListener) fragment).onCarUiInsetsChanged(insets);
198                             handled = true;
199                         }
200                     }
201                 }
202             }
203 
204             if (!handled) {
205                 mContentView.setPadding(insets.getLeft(), insets.getTop(),
206                         insets.getRight(), insets.getBottom());
207             }
208         }
209     }
210 }
211