blob: 03f7307cf437fe58c986736a800833fb6cfcbe66 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.ui.sharedlibrary;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.loader.ResourcesLoader;
import android.content.res.loader.ResourcesProvider;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.car.ui.sharedlibrary.oemapis.SharedLibraryVersionProviderOEMV1;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This is an implementation of {@link SharedLibraryVersionProviderOEMV1} that proxies it's methods
* to a different {@link SharedLibraryVersionProviderOEMV1} that is loaded from a different apk.
*
* This proxy is so that the real {@link SharedLibraryVersionProviderOEMV1} and all the classes
* it depends on are not on the classpath. Since shared libraries come before the app on the
* classpath, this could produce version conflicts between different versions of static libraries
* compiled into both the app and the shared library.
*
* The real SharedLibraryVersionProviderOEMV1's classloader's parent classloader is the classloader
* used to load this class. That way, the real shared library can still access the classes defined
* in this "proxy" shared library. Since shared library classloaders are isolated, and cannot
* access the app's classes, this does not produce the same version conflict mentioned before.
*/
public final class SharedLibraryVersionProviderImpl implements SharedLibraryVersionProviderOEMV1 {
private static final String TAG = "carui";
// This should match with the package name in the <queries/> in the manifest.
private static final String OEM_SHAREDLIB_PACKAGENAME =
"com.google.car.ui.sharedlibrary";
private static ClassLoader sCachedClassLoader;
private static boolean sHasInjectedResources = false;
private Object mOEMVersionProvider;
@Override
public synchronized Object getSharedLibraryFactory(int maxVersion, Context context) {
ApplicationInfo appInfo = getApplicationInfo(context, OEM_SHAREDLIB_PACKAGENAME);
ClassLoader cl = getClassLoader(appInfo);
if (!sHasInjectedResources) {
injectResources(cl, context, appInfo);
sHasInjectedResources = true;
}
try {
if (mOEMVersionProvider == null) {
mOEMVersionProvider = cl
.loadClass(OEM_SHAREDLIB_PACKAGENAME + ".SharedLibraryVersionProviderImpl")
.getDeclaredConstructor()
.newInstance();
}
return mOEMVersionProvider.getClass()
.getDeclaredMethod("getSharedLibraryFactory", int.class, Context.class)
.invoke(mOEMVersionProvider, maxVersion, context);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(
"Could not call SharedLibraryVersionProviderImpl.getSharedLibraryFactory()", e);
}
}
/**
* Injects the resources belonging to the {@link ApplicationInfo} into the provided
* {@link Context}.
*/
private static void injectResources(ClassLoader cl, Context context, ApplicationInfo appInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// TODO(b/175649937): Add a solution for P/Q.
throw new UnsupportedOperationException(
"Shared library cannot run pre R at the moment");
}
try {
ResourcesLoader rl = new ResourcesLoader();
File apk = new File(appInfo.sourceDir);
rl.addProvider(
ResourcesProvider.loadFromApk(
ParcelFileDescriptor.open(apk, ParcelFileDescriptor.MODE_READ_ONLY)));
context.getResources().addLoaders(rl);
Class<?> assetManagerClazz = AssetManager.class;
Method getAssignedPackageIdentifiers =
assetManagerClazz.getDeclaredMethod("getAssignedPackageIdentifiers");
getAssignedPackageIdentifiers.setAccessible(true);
SparseArray<String> packageIdentifiers =
(SparseArray<String>) getAssignedPackageIdentifiers
.invoke(context.getResources().getAssets());
for (int i = 0; i < packageIdentifiers.size(); i++) {
if (OEM_SHAREDLIB_PACKAGENAME.equals(packageIdentifiers.valueAt(i))) {
rewriteRValues(cl, packageIdentifiers.valueAt(i), packageIdentifiers.keyAt(i));
break;
}
}
} catch (IOException | ReflectiveOperationException | SecurityException e) {
throw new RuntimeException("Unable to load shared library resources", e);
}
}
/**
* Exact copy from {@link LoadedApk}
*/
private static void rewriteRValues(ClassLoader cl, String packageName, int id) {
final Class<?> rClazz;
try {
rClazz = cl.loadClass(packageName + ".R");
} catch (ClassNotFoundException e) {
// This is not necessarily an error, as some packages do not ship with resources
// (or they do not need rewriting).
Log.i(TAG, "No resource references to update in package " + packageName);
return;
}
final Method callback;
try {
callback = rClazz.getMethod("onResourcesLoaded", int.class);
} catch (NoSuchMethodException e) {
// No rewriting to be done.
return;
}
Throwable cause;
try {
callback.invoke(null, id);
return;
} catch (IllegalAccessException e) {
cause = e;
} catch (InvocationTargetException e) {
cause = e.getCause();
}
throw new RuntimeException("Failed to rewrite resource references for " + packageName,
cause);
}
private static ApplicationInfo getApplicationInfo(Context context, String packageName) {
try {
return context.getPackageManager().getApplicationInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Package not found: " + packageName, e);
}
}
/**
* Returns a classloader that loads classes from the apk specificed by the
* {@link ApplicationInfo}.
*/
private static ClassLoader getClassLoader(ApplicationInfo appInfo) {
if (sCachedClassLoader != null) {
return sCachedClassLoader;
}
List<String> libraryPaths = new ArrayList<>(3);
if (appInfo.nativeLibraryDir != null) {
libraryPaths.add(appInfo.nativeLibraryDir);
}
if ((appInfo.flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) == 0) {
for (String abi : getSupportedAbisForCurrentRuntime()) {
libraryPaths.add(appInfo.sourceDir + "!/lib/" + abi);
}
}
String flatLibraryPaths = (libraryPaths.size() == 0
? null : TextUtils.join(File.pathSeparator, libraryPaths));
String apkPaths = appInfo.sourceDir;
if (appInfo.sharedLibraryFiles != null && appInfo.sharedLibraryFiles.length > 0) {
// Unless you pass PackageManager.GET_SHARED_LIBRARY_FILES this will always be null
// HOWEVER, if you running on a device with F5 active, the module's dex files are
// always listed in ApplicationInfo.sharedLibraryFiles and should be included in
// the classpath.
apkPaths +=
File.pathSeparator + TextUtils.join(File.pathSeparator,
appInfo.sharedLibraryFiles);
}
sCachedClassLoader = new PathClassLoader(apkPaths, flatLibraryPaths,
SharedLibraryVersionProviderImpl.class.getClassLoader());
return sCachedClassLoader;
}
private static List<String> getSupportedAbisForCurrentRuntime() {
List<String> abis = new ArrayList<>();
if (Process.is64Bit()) {
Collections.addAll(abis, Build.SUPPORTED_64_BIT_ABIS);
} else {
Collections.addAll(abis, Build.SUPPORTED_32_BIT_ABIS);
}
return abis;
}
}