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