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 
20 import android.annotation.SuppressLint;
21 import android.app.Activity;
22 import android.app.Application;
23 import android.content.ComponentName;
24 import android.content.ContentProvider;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.util.Log;
31 import android.view.LayoutInflater;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.RequiresApi;
36 import androidx.appcompat.app.AppCompatDelegate;
37 
38 import com.android.car.ui.CarUiLayoutInflaterFactory;
39 import com.android.car.ui.R;
40 import com.android.car.ui.baselayout.Insets;
41 import com.android.car.ui.utils.CarUiUtils;
42 
43 import java.lang.reflect.Constructor;
44 import java.lang.reflect.Method;
45 import java.util.Arrays;
46 import java.util.HashSet;
47 
48 /**
49  * {@link ContentProvider ContentProvider's} onCreate() methods are "called for all registered
50  * content providers on the application main thread at application launch time." This means we
51  * can use a content provider to register for Activity lifecycle callbacks before any activities
52  * have started, for installing the CarUi base layout into all activities.
53  *
54  * Notice that in many of the methods in this class we're using reflection to make method calls.
55  * As it's explained in (b/156532465), {@link CarUiInstaller} is loaded from
56  * GMSCore's ContainerActivity classloader which is different than the classloader of the Activity
57  * that's passed as an argument to these methods. This happens when the Activity's module is loaded
58  * dynamically. That means {@link CarUiInstaller} will have a different classloader than the
59  * Activity. Hence we will need to use the Activity's classloader to load
60  * {@link BaseLayoutController} class otherwise the base layout will be loaded
61  * by the wrong classloader. And then calls to {@see CarUi#getToolbar(Activity)} will return null.
62  */
63 // TODO: (b/200322953)
64 @SuppressLint("LogConditional")
65 @RequiresApi(MIN_TARGET_API)
66 public class CarUiInstaller extends ContentProvider {
67 
68     private static final String TAG = "CarUiInstaller";
69     private static final String CAR_UI_INSET_LEFT = "CAR_UI_INSET_LEFT";
70     private static final String CAR_UI_INSET_RIGHT = "CAR_UI_INSET_RIGHT";
71     private static final String CAR_UI_INSET_TOP = "CAR_UI_INSET_TOP";
72     private static final String CAR_UI_INSET_BOTTOM = "CAR_UI_INSET_BOTTOM";
73 
74     // applications against which we have already called register
75     private static final HashSet<Application> sAppsRegistered = new HashSet<Application>();
76 
hasAlreadyRegistered(Application application)77     private static boolean hasAlreadyRegistered(Application application) {
78         synchronized (sAppsRegistered) {
79             return !sAppsRegistered.add(application);
80         }
81     }
82 
83     @Override
onCreate()84     public boolean onCreate() {
85         Context context = getContext();
86         if (context == null || !(context.getApplicationContext() instanceof Application)) {
87             Log.e(TAG, "CarUiInstaller had a null context, unable to call register!"
88                         + " Need app to call register by itself");
89             return false;
90         }
91 
92         Application application = (Application) context.getApplicationContext();
93         register(application);
94 
95         return true;
96     }
97 
98     /**
99      * In some cases {@link CarUiInstaller#onCreate} is called before the {@link Application}
100      * instance is created. In those cases applications have to call this method separately
101      * after the Application instance is fully initialized.
102      */
register(@onNull Application application)103     public static void register(@NonNull Application application) {
104         if (hasAlreadyRegistered(application)) {
105             return;
106         }
107 
108         application.registerActivityLifecycleCallbacks(
109                 new Application.ActivityLifecycleCallbacks() {
110                     private Insets mInsets = null;
111                     private boolean mIsActivityStartedForFirstTime = false;
112 
113                     private boolean shouldRun(Activity activity) {
114                         return CarUiUtils.getThemeBoolean(activity, R.attr.carUiActivity);
115                     }
116 
117                     @Override
118                     public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
119                         if (!shouldRun(activity)) {
120                             return;
121                         }
122 
123                         ComponentName comp = ComponentName.createRelative(
124                                 activity, activity.getClass().getName());
125                         Log.i(TAG, "CarUiInstaller started for " + comp.flattenToShortString());
126 
127                         injectLayoutInflaterFactory(activity);
128 
129                         callMethodReflective(
130                                 activity.getClassLoader(),
131                                 BaseLayoutController.class,
132                                 "build",
133                                 null,
134                                 activity);
135 
136                         if (savedInstanceState != null) {
137                             int inset_left = savedInstanceState.getInt(CAR_UI_INSET_LEFT);
138                             int inset_top = savedInstanceState.getInt(CAR_UI_INSET_TOP);
139                             int inset_right = savedInstanceState.getInt(CAR_UI_INSET_RIGHT);
140                             int inset_bottom = savedInstanceState.getInt(CAR_UI_INSET_BOTTOM);
141                             mInsets = new Insets(inset_left, inset_top, inset_right, inset_bottom);
142                         }
143 
144                         mIsActivityStartedForFirstTime = true;
145                     }
146 
147                     @Override
148                     public void onActivityPostStarted(Activity activity) {
149                         if (!shouldRun(activity)) {
150                             return;
151                         }
152 
153                         Object controller = callMethodReflective(
154                                 activity.getClassLoader(),
155                                 BaseLayoutController.class,
156                                 "getBaseLayoutController",
157                                 null,
158                                 activity);
159                         if (mInsets != null && controller != null
160                                 && mIsActivityStartedForFirstTime) {
161                             callMethodReflective(
162                                     activity.getClassLoader(),
163                                     BaseLayoutController.class,
164                                     "dispatchNewInsets",
165                                     controller,
166                                     changeInsetsClassLoader(activity.getClassLoader(), mInsets));
167                             mIsActivityStartedForFirstTime = false;
168                         }
169                     }
170 
171                     @Override
172                     public void onActivityStarted(Activity activity) {
173                     }
174 
175                     @Override
176                     public void onActivityResumed(Activity activity) {
177                     }
178 
179                     @Override
180                     public void onActivityPaused(Activity activity) {
181                     }
182 
183                     @Override
184                     public void onActivityStopped(Activity activity) {
185                     }
186 
187                     @Override
188                     public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
189                         if (!shouldRun(activity)) {
190                             return;
191                         }
192 
193                         Object controller = callMethodReflective(
194                                 activity.getClassLoader(),
195                                 BaseLayoutController.class,
196                                 "getBaseLayoutController",
197                                 null,
198                                 activity);
199                         if (controller != null) {
200                             Object insets = callMethodReflective(
201                                     activity.getClassLoader(),
202                                     BaseLayoutController.class,
203                                     "getInsets",
204                                     controller);
205                             outState.putInt(CAR_UI_INSET_LEFT,
206                                     (int) callMethodReflective(
207                                             activity.getClassLoader(),
208                                             Insets.class,
209                                             "getLeft",
210                                             insets));
211                             outState.putInt(CAR_UI_INSET_TOP,
212                                     (int) callMethodReflective(
213                                             activity.getClassLoader(),
214                                             Insets.class,
215                                             "getTop",
216                                             insets));
217                             outState.putInt(CAR_UI_INSET_RIGHT,
218                                     (int) callMethodReflective(
219                                             activity.getClassLoader(),
220                                             Insets.class,
221                                             "getRight",
222                                             insets));
223                             outState.putInt(CAR_UI_INSET_BOTTOM,
224                                     (int) callMethodReflective(
225                                             activity.getClassLoader(),
226                                             Insets.class,
227                                             "getBottom",
228                                             insets));
229                         }
230                     }
231 
232                     @Override
233                     public void onActivityDestroyed(Activity activity) {
234                         if (!shouldRun(activity)) {
235                             return;
236                         }
237 
238                         callMethodReflective(
239                                 activity.getClassLoader(),
240                                 BaseLayoutController.class,
241                                 "destroy",
242                                 null,
243                                 activity);
244                     }
245                 });
246     }
247 
248     @Nullable
249     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)250     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
251             @Nullable String[] selectionArgs, @Nullable String sortOrder) {
252         return null;
253     }
254 
255     @Nullable
256     @Override
getType(@onNull Uri uri)257     public String getType(@NonNull Uri uri) {
258         return null;
259     }
260 
261     @Nullable
262     @Override
insert(@onNull Uri uri, @Nullable ContentValues values)263     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
264         return null;
265     }
266 
267     @Override
delete(@onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)268     public int delete(@NonNull Uri uri, @Nullable String selection,
269             @Nullable String[] selectionArgs) {
270         return 0;
271     }
272 
273     @Override
update(@onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)274     public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
275             @Nullable String[] selectionArgs) {
276         return 0;
277     }
278 
279     @SuppressWarnings("AndroidJdkLibsChecker")
callMethodReflective(@onNull ClassLoader cl, @NonNull Class<?> srcClass, @NonNull String methodName, @Nullable Object instance, @Nullable Object... args)280     private static Object callMethodReflective(@NonNull ClassLoader cl, @NonNull Class<?> srcClass,
281             @NonNull String methodName, @Nullable Object instance, @Nullable Object... args) {
282         try {
283             Class<?> clazz = cl.loadClass(srcClass.getName());
284             Class<?>[] classArgs = args == null ? null
285                     : Arrays.stream(args)
286                             .map(arg -> arg instanceof Activity ? Activity.class : arg.getClass())
287                             .toArray(Class<?>[]::new);
288             Method method = clazz.getDeclaredMethod(methodName, classArgs);
289             method.setAccessible(true);
290             return method.invoke(instance, args);
291         } catch (ReflectiveOperationException | SecurityException e) {
292             throw new RuntimeException(e);
293         }
294     }
295 
changeInsetsClassLoader(@onNull ClassLoader cl, @Nullable Insets src)296     private static Object changeInsetsClassLoader(@NonNull ClassLoader cl, @Nullable Insets src) {
297         if (src == null) {
298             return null;
299         }
300         try {
301             Class<?> insetsClass = cl.loadClass(Insets.class.getName());
302             Constructor<?> cnst = insetsClass.getDeclaredConstructor(
303                     int.class,
304                     int.class,
305                     int.class,
306                     int.class);
307             cnst.setAccessible(true);
308             return cnst.newInstance(
309                     src.getLeft(),
310                     src.getTop(),
311                     src.getRight(),
312                     src.getBottom());
313         } catch (ReflectiveOperationException | SecurityException e) {
314             throw new RuntimeException();
315         }
316     }
317 
injectLayoutInflaterFactory(Context context)318     private static void injectLayoutInflaterFactory(Context context) {
319         // For {@link AppCompatActivity} activities our layout inflater
320         // factory is instantiated via viewInflaterClass attribute.
321         LayoutInflater layoutInflater = LayoutInflater.from(context);
322         if (layoutInflater.getFactory2() == null) {
323             layoutInflater.setFactory2(new CarUiLayoutInflaterFactory());
324         } else if (!(layoutInflater.getFactory2()
325                 instanceof CarUiLayoutInflaterFactory)
326                         && !(layoutInflater.getFactory2()
327                                 instanceof AppCompatDelegate)) {
328             throw new AssertionError(layoutInflater.getFactory2()
329                     + " must extend CarUiLayoutInflaterFactory");
330         }
331     }
332 }
333