blob: 93b5501fe34bc086af8b65881a276f212ec2a182 [file] [log] [blame]
package org.jetbrains.android.uipreview;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.builder.model.Variant;
import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.rendering.RenderSecurityManager;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.editors.theme.ThemeEditorProvider;
import com.android.tools.idea.editors.theme.ThemeEditorUtils;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.rendering.AarResourceClassRegistry;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.RenderClassLoader;
import com.android.utils.SdkUtils;
import com.google.common.collect.Maps;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.CompilerModuleExtension;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.containers.HashSet;
import com.intellij.util.containers.WeakHashMap;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AndroidRootUtil;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidTargetData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.android.SdkConstants.*;
import static org.jetbrains.android.facet.ResourceFolderManager.EXPLODED_AAR;
/**
* Render class loader responsible for loading classes in custom views
* and local and library classes used by those custom views (other than
* the framework itself, which is loaded by a parent class loader via
* layout lib.)
*/
public final class ModuleClassLoader extends RenderClassLoader {
private static final Logger LOG = Logger.getInstance(ModuleClassLoader.class);
public static final boolean DEBUG_CLASS_LOADING = false;
/** The base module to use as a render context; the class loader will consult the module dependencies and library dependencies
* of this class as well to find classes */
private final Module myModule;
/** The layout library to use as a root class loader (e.g. the place to obtain the layoutlib Android SDK view classes from */
private final LayoutLibrary myLibrary;
/** Map from fully qualified class name to the corresponding .class file for each class loaded by this class loader */
private Map<String,File> myClassFiles;
/** Map from fully qualified class name to the corresponding last modified file stamp for each class loaded by this class loader */
private Map<String,Long> myLoadedTimestamp;
private ModuleClassLoader(@NotNull LayoutLibrary library, @NotNull Module module) {
super(library.getClassLoader());
myLibrary = library;
myModule = module;
}
@NotNull
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
if (!myInsideJarClassLoader) {
int index = name.lastIndexOf('.');
if (index != -1 && name.charAt(index + 1) == 'R' && (index == name.length() - 2 || name.charAt(index + 2) == '$') && index > 1) {
AppResourceRepository appResources = AppResourceRepository.getAppResources(myModule, false);
if (appResources != null) {
byte[] data = AarResourceClassRegistry.get(myModule.getProject()).findClassDefinition(name, appResources);
if (data != null) {
data = convertClass(data);
if (DEBUG_CLASS_LOADING) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println(" defining class " + name + " from AAR registry");
}
return defineClassAndPackage(name, data, 0, data.length);
}
}
throw e;
}
}
byte[] clazz = null;
if (RecyclerViewHelper.CN_CUSTOM_ADAPTER.equals(name)) {
clazz = RecyclerViewHelper.getAdapterClass();
}
if (RecyclerViewHelper.CN_CUSTOM_VIEW_HOLDER.equals(name)) {
clazz = RecyclerViewHelper.getViewHolder();
}
if (clazz != null) {
return defineClassAndPackage(name, clazz, 0, clazz.length);
}
throw e;
}
}
@Nullable
public static ClassLoader create(IAndroidTarget target, Module module) throws Exception {
AndroidPlatform androidPlatform = AndroidPlatform.getInstance(module);
if (androidPlatform == null) {
return null;
}
AndroidTargetData targetData = androidPlatform.getSdkData().getTargetData(target);
LayoutLibrary library = targetData.getLayoutLibrary(module.getProject());
if (library == null) {
return null;
}
return get(library, module);
}
@NotNull
@Override
protected Class<?> load(String name) throws ClassNotFoundException {
final Class<?> aClass = loadClassFromModuleOrDependency(myModule, name, new HashSet<Module>());
if (aClass != null) {
return aClass;
}
throw new ClassNotFoundException(name);
}
@Nullable
private Class<?> loadClassFromModuleOrDependency(Module module, String name, Set<Module> visited) {
if (!visited.add(module)) {
return null;
}
Class<?> aClass = loadClassFromModule(module, name);
if (aClass != null) {
return aClass;
}
aClass = loadClassFromJar(name);
if (aClass != null) {
return aClass;
}
for (Module depModule : ModuleRootManager.getInstance(module).getDependencies(false)) {
aClass = loadClassFromModuleOrDependency(depModule, name, visited);
if (aClass != null) {
return aClass;
}
}
return null;
}
@Nullable
private Class<?> loadClassFromModule(Module module, String name) {
final CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module);
if (extension == null) {
return null;
}
VirtualFile vOutFolder = extension.getCompilerOutputPath();
if (vOutFolder == null) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null && facet.requiresAndroidModel()) {
// Try a bit harder; we don't have a compiler module extension or mechanism
// to query this yet, so just hardcode it (ugh!)
IdeaAndroidProject androidModel = facet.getAndroidModel();
if (androidModel != null) {
Variant variant = androidModel.getSelectedVariant();
String variantName = variant.getName();
AndroidArtifact mainArtifactInfo = androidModel.getMainArtifact();
File classesFolder = mainArtifactInfo.getClassesFolder();
// Older models may not supply it; in that case, we rely on looking relative
// to the .APK file location:
//noinspection ConstantConditions
if (classesFolder == null) {
@SuppressWarnings("deprecation") // For getOutput()
AndroidArtifactOutput output = GradleUtil.getOutput(mainArtifactInfo);
File file = output.getMainOutputFile().getOutputFile();
File buildFolder = file.getParentFile().getParentFile();
classesFolder = new File(buildFolder, "classes"); // See AndroidContentRoot
}
File outFolder = new File(classesFolder,
// Change variant name variant-release into variant/release directories
variantName.replace('-', File.separatorChar));
if (outFolder.exists()) {
vOutFolder = LocalFileSystem.getInstance().findFileByIoFile(outFolder);
if (vOutFolder != null) {
Class<?> localClass = loadClassFromClassPath(name, VfsUtilCore.virtualToIoFile(vOutFolder));
if (localClass != null) {
return localClass;
}
}
}
}
}
return null;
}
return loadClassFromClassPath(name, VfsUtilCore.virtualToIoFile(vOutFolder));
}
/**
* Determines whether the class specified by the given qualified name has a source file in the IDE that
* has been edited more recently than its corresponding class file.
*
* <b></b>Note that this method can only answer queries for classes that this class loader has previously
* loaded!</b>
*
* @param fqcn the fully qualified class name
* @param myCredential a render sandbox credential
* @return true if the source file has been modified, or false if not (or if the source file cannot be found)
*/
public boolean isSourceModified(@NotNull final String fqcn, @Nullable final Object myCredential) {
File classFile = getClassFile(fqcn);
// Make sure the class file is up to date and if not, log an error
if (classFile != null) {
// Allow creating class loaders during rendering; may be prevented by the RenderSecurityManager
boolean token = RenderSecurityManager.enterSafeRegion(myCredential);
try {
long classFileModified = classFile.lastModified();
if (classFileModified > 0L) {
VirtualFile virtualFile = ApplicationManager.getApplication().runReadAction(new Computable<VirtualFile>() {
@Nullable
@Override
public VirtualFile compute() {
Project project = myModule.getProject();
GlobalSearchScope scope = myModule.getModuleWithDependenciesScope();
PsiManager psiManager = PsiManager.getInstance(project);
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(psiManager.getProject());
PsiClass source = psiFacade.findClass(fqcn, scope);
if (source != null) {
PsiFile containingFile = source.getContainingFile();
if (containingFile != null) {
return containingFile.getVirtualFile();
}
}
return null;
}
});
if (virtualFile != null && !FN_RESOURCE_CLASS.equals(virtualFile.getName())) { // Don't flag R.java edits; not done by user
// Edited but not yet saved?
boolean modified = FileDocumentManager.getInstance().isFileModified(virtualFile);
if (!modified) {
// Check timestamp
File sourceFile = VfsUtilCore.virtualToIoFile(virtualFile);
long sourceFileModified = sourceFile.lastModified();
AndroidFacet facet = AndroidFacet.getInstance(myModule);
// User modifications on the source file might not always result on a new .class file.
// If it's a gradle project, we use the project modification time instead to display the warning
// more reliably.
long lastBuildTimestamp = facet != null && facet.requiresAndroidModel()
? PostProjectBuildTasksExecutor.getInstance(myModule.getProject()).getLastBuildTimestamp()
: classFileModified;
if (sourceFileModified > lastBuildTimestamp && lastBuildTimestamp != -1) {
modified = true;
}
}
return modified;
}
}
} finally {
RenderSecurityManager.exitSafeRegion(token);
}
}
return false;
}
@Override
@Nullable
protected Class<?> loadClassFile(final String fqcn, File classFile) {
if (myClassFiles == null) {
myClassFiles = Maps.newHashMap();
myLoadedTimestamp = Maps.newHashMap();
}
myClassFiles.put(fqcn, classFile);
myLoadedTimestamp.put(fqcn, classFile.lastModified());
return super.loadClassFile(fqcn, classFile);
}
@Override
protected List<URL> getExternalJars() {
final List<URL> result = new ArrayList<URL>();
if (ThemeEditorProvider.THEME_EDITOR_ENABLE) {
URL customWidgetsUrl = ThemeEditorUtils.getCustomWidgetsJarUrl();
if (customWidgetsUrl != null) {
result.add(customWidgetsUrl);
}
}
for (VirtualFile libFile : AndroidRootUtil.getExternalLibraries(myModule)) {
if (EXT_JAR.equals(libFile.getExtension())) {
final File file = new File(libFile.getPath());
if (file.exists()) {
try {
result.add(SdkUtils.fileToUrl(file));
File aarDir = file.getParentFile();
if (aarDir != null && (aarDir.getPath().endsWith(DOT_AAR) ||
aarDir.getPath().contains(EXPLODED_AAR))) {
if (aarDir.getPath().contains(EXPLODED_AAR) && FD_JARS.equals(aarDir.getName())) {
// Gradle plugin version 1.2.x and later has classes in aar-dir/jars/
aarDir = aarDir.getParentFile();
}
AppResourceRepository appResources = AppResourceRepository.getAppResources(myModule, true);
if (appResources != null) {
AarResourceClassRegistry.get(myModule.getProject()).addLibrary(appResources, aarDir);
}
}
}
catch (MalformedURLException e) {
LOG.error(e);
}
}
}
}
return result;
}
/** Returns the path to a class file loaded for the given class, if any */
@Nullable
public File getClassFile(@NotNull String className) {
return myClassFiles != null ? myClassFiles.get(className) : null;
}
/** Checks whether any of the .class files loaded by this loader have changed since the creation of this class loader */
private boolean isUpToDate() {
if (myClassFiles != null) {
for (Map.Entry<String, File> entry : myClassFiles.entrySet()) {
String className = entry.getKey();
File classFile = entry.getValue();
Long timestamp = myLoadedTimestamp.get(className);
if (timestamp != null) {
long loadedModifiedTime = timestamp;
long classFileModified = classFile.lastModified();
if (classFileModified > 0L && loadedModifiedTime > 0L && loadedModifiedTime < classFileModified) {
return false;
}
}
}
}
return true;
}
/**
* Returns a project class loader to use for rendering. May cache instances across render sessions.
*/
@NotNull
public static ModuleClassLoader get(@NotNull LayoutLibrary library, @NotNull Module module) {
ModuleClassLoader loader = ourCache.get(module);
if (loader != null) {
if (library != loader.myLibrary) {
if (DEBUG_CLASS_LOADING) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println("Discarding loader because the layout library has changed");
}
loader = null;
} else if (!loader.isUpToDate()) {
if (DEBUG_CLASS_LOADING) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println("Discarding loader because some files have changed");
}
loader = null;
} else {
List<URL> updatedJarDependencies = loader.getExternalJars();
if (loader.myJarClassLoader != null && !updatedJarDependencies.equals(loader.myJarClassLoader.getUrls())) {
if (DEBUG_CLASS_LOADING) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println("Recreating jar class loader because dependencies have changed.");
}
loader.myJarClassLoader = loader.createClassLoader(updatedJarDependencies);
}
}
// To be correct we should also check that the dependencies have not changed. It's not
// a problem when you add a new dependency with classes that haven't yet been resolved,
// but if for some reason a class is defined in multiple dependencies, and we've already
// resolved that class, changing the order could change the particular class that should
// be loaded, and we would need to recreate the loader - but this won't detect that.
// To solve this correctly we'd need to store a key in the cache recording the project
// dependencies when the loader was created, and compare it to the current one. However,
// that's a pretty unusual scenario so we won't worry about it until the loader situation
// is solved more generally (e.g. separating out .jar loading from .class loading etc).
}
if (loader == null) {
loader = new ModuleClassLoader(library, module);
ourCache.put(module, loader);
} else if (DEBUG_CLASS_LOADING) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println("Reused class loader for rendering");
}
return loader;
}
/** Flush any cached class loaders */
public static void clearCache() {
ourCache.clear();
}
/** Remove the cached class loader for the module. */
public static void clearCache(Module module) {
if (ourCache.containsKey(module)) {
ourCache.remove(module);
}
}
/** Temporary hack: Store this in a weak hash map cached by modules. In the next version we should move this
* into a proper persistent render service. */
private static WeakHashMap<Module,ModuleClassLoader> ourCache = new WeakHashMap<Module, ModuleClassLoader>();
}