/* * Copyright (C) 2013 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 org.jetbrains.android.uipreview; import com.android.SdkConstants; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.RenderSecurityManager; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.resources.IntArrayWrapper; import com.android.resources.ResourceType; import com.android.tools.idea.rendering.AppResourceRepository; import com.android.tools.idea.rendering.InconvertibleClassError; import com.android.tools.idea.rendering.RenderLogger; import com.android.util.Pair; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.util.Computable; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.util.containers.HashSet; import gnu.trove.TIntObjectHashMap; import gnu.trove.TObjectIntHashMap; import org.jetbrains.android.dom.layout.FragmentLayoutDomFileDescription; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.*; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import java.util.Set; import static com.android.SdkConstants.*; /** * Handler for loading views for the layout editor on demand, and reporting issues with class * loading, instance creation, etc. */ @SuppressWarnings("deprecation") // The Pair class is required by the IProjectCallback public class ViewLoader { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.uipreview.ViewLoader"); @NotNull private final Module myModule; @NotNull private final Map<String, Class<?>> myLoadedClasses = new HashMap<String, Class<?>>(); @Nullable private final Object myCredential; @NotNull private RenderLogger myLogger; @Nullable private final ClassLoader myParentClassLoader; @Nullable private ProjectClassLoader myProjectClassLoader; public ViewLoader(@NotNull LayoutLibrary layoutLib, @NotNull AndroidFacet facet, @NotNull RenderLogger logger, @Nullable Object credential) { myParentClassLoader = layoutLib.getClassLoader(); myModule = facet.getModule(); myLogger = logger; myCredential = credential; } /** * Sets the {@link LayoutLog} logger to use for error messages during problems * * @param logger the new logger to use, or null to clear it out */ public void setLogger(@Nullable RenderLogger logger) { myLogger = logger; } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Nullable public Object loadView(String className, Class[] constructorSignature, Object[] constructorArgs) throws ClassNotFoundException { Class<?> aClass = myLoadedClasses.get(className); try { if (aClass != null) { return createNewInstance(aClass, constructorSignature, constructorArgs); } aClass = loadClass(className); if (aClass != null) { final Object viewObject = createNewInstance(aClass, constructorSignature, constructorArgs); myLoadedClasses.put(className, aClass); return viewObject; } } catch (InconvertibleClassError e) { myLogger.addIncorrectFormatClass(e.getClassName(), e); } catch (LinkageError e) { myLogger.addBrokenClass(className, e); } catch (ClassNotFoundException e) { myLogger.addBrokenClass(className, e); } catch (InvocationTargetException e) { final Throwable cause = e.getCause(); if (cause instanceof InconvertibleClassError) { InconvertibleClassError error = (InconvertibleClassError)cause; myLogger.addIncorrectFormatClass(error.getClassName(), error); } else { myLogger.addBrokenClass(className, cause); } } catch (IllegalAccessException e) { myLogger.addBrokenClass(className, e); } catch (InstantiationException e) { myLogger.addBrokenClass(className, e); } catch (NoSuchMethodException e) { myLogger.addBrokenClass(className, e); } try { final Object o = createViewFromSuperclass(className, constructorSignature, constructorArgs); if (o != null) { return o; } return createMockView(className, constructorSignature, constructorArgs); } catch (ClassNotFoundException e) { throw new ClassNotFoundException(className, e); } catch (InvocationTargetException e) { throw new ClassNotFoundException(className, e); } catch (NoSuchMethodException e) { throw new ClassNotFoundException(className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException(className, e); } catch (InstantiationException e) { throw new ClassNotFoundException(className, e); } catch (NoSuchFieldException e) { throw new ClassNotFoundException(className, e); } } @Nullable private Class<?> loadClass(String className) throws InconvertibleClassError { try { return getProjectClassLoader().loadClass(className); } catch (ClassNotFoundException e) { if (!className.equals(FragmentLayoutDomFileDescription.FRAGMENT_TAG_NAME)) { myLogger.addMissingClass(className); } return null; } } @NotNull private ProjectClassLoader getProjectClassLoader() { if (myProjectClassLoader == null) { // Allow creating class loaders during rendering; may be prevented by the RenderSecurityManager boolean token = RenderSecurityManager.enterSafeRegion(myCredential); try { myProjectClassLoader = new ProjectClassLoader(myParentClassLoader, myModule, myLogger, myCredential); } finally { RenderSecurityManager.exitSafeRegion(token); } } return myProjectClassLoader; } @Nullable private Object createViewFromSuperclass(final String className, final Class[] constructorSignature, final Object[] constructorArgs) { return ApplicationManager.getApplication().runReadAction(new Computable<Object>() { @Nullable @Override public Object compute() { final JavaPsiFacade facade = JavaPsiFacade.getInstance(myModule.getProject()); PsiClass psiClass = facade.findClass(className, myModule.getModuleWithDependenciesAndLibrariesScope(false)); if (psiClass == null) { return null; } psiClass = psiClass.getSuperClass(); final Set<String> visited = new HashSet<String>(); while (psiClass != null) { final String qName = psiClass.getQualifiedName(); if (qName == null || !visited.add(qName) || AndroidUtils.VIEW_CLASS_NAME.equals(psiClass.getQualifiedName())) { break; } if (!AndroidUtils.isAbstract(psiClass)) { try { Class<?> aClass = myLoadedClasses.get(qName); if (aClass == null && myParentClassLoader != null) { aClass = myParentClassLoader.loadClass(qName); if (aClass != null) { myLoadedClasses.put(qName, aClass); } } if (aClass != null) { final Object instance = createNewInstance(aClass, constructorSignature, constructorArgs); if (instance != null) { return instance; } } } catch (Throwable e) { LOG.debug(e); } } psiClass = psiClass.getSuperClass(); } return null; } }); } private Object createMockView(String className, Class[] constructorSignature, Object[] constructorArgs) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, NoSuchFieldException { final Class<?> mockViewClass = getProjectClassLoader().loadClass(SdkConstants.CLASS_MOCK_VIEW); final Object viewObject = createNewInstance(mockViewClass, constructorSignature, constructorArgs); final Method setTextMethod = viewObject.getClass().getMethod("setText", CharSequence.class); String label = getShortClassName(className); if (label.equals(VIEW_FRAGMENT)) { label = "<fragment>"; // TODO: // Append "\nPick preview layout from the \"Fragment Layout\" context menu" // when used from the layout editor } else if (label.equals(VIEW_INCLUDE)) { label = "Text"; } setTextMethod.invoke(viewObject, label); try { final Class<?> gravityClass = Class.forName("android.view.Gravity", true, viewObject.getClass().getClassLoader()); final Field centerField = gravityClass.getField("CENTER"); final int center = centerField.getInt(null); final Method setGravityMethod = viewObject.getClass().getMethod("setGravity", Integer.TYPE); setGravityMethod.invoke(viewObject, Integer.valueOf(center)); } catch (ClassNotFoundException e) { LOG.info(e); } return viewObject; } @NotNull public Module getModule() { return myModule; } private static String getShortClassName(String fqcn) { if (fqcn.startsWith("android.")) { // android.foo.Name -> android...Name int first = fqcn.indexOf('.'); int last = fqcn.lastIndexOf('.'); if (last > first) { return fqcn.substring(0, first) + ".." + fqcn.substring(last); } } else { // com.example.p1.p2.MyClass -> com.example...MyClass int first = fqcn.indexOf('.'); first = fqcn.indexOf('.', first + 1); int last = fqcn.lastIndexOf('.'); if (last > first && first >= 0) { return fqcn.substring(0, first) + ".." + fqcn.substring(last); } } return fqcn; } @SuppressWarnings("ConstantConditions") private Object createNewInstance(Class<?> clazz, Class[] constructorSignature, Object[] constructorParameters) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException { Constructor<?> constructor = null; try { constructor = clazz.getConstructor(constructorSignature); } catch (NoSuchMethodException e) { // View class has 1-parameter, 2-parameter and 3-parameter constructors final int paramsCount = constructorSignature.length; if (paramsCount == 0) { throw e; } for (int i = 3; i >= 1; i--) { if (i == paramsCount) { continue; } final int k = paramsCount < i ? paramsCount : i; final Class[] sig = new Class[i]; System.arraycopy(constructorSignature, 0, sig, 0, k); final Object[] params = new Object[i]; System.arraycopy(constructorParameters, 0, params, 0, k); for (int j = k + 1; j <= i; j++) { if (j == 2) { sig[j - 1] = clazz.getClassLoader().loadClass("android.util.AttributeSet"); params[j - 1] = null; } else if (j == 3) { // parameter 3: int defstyle sig[j - 1] = int.class; params[j - 1] = 0; } } constructorSignature = sig; constructorParameters = params; try { constructor = clazz.getConstructor(constructorSignature); if (constructor != null) { if (constructorSignature.length < 2) { LOG.info("wrong_constructor: Custom view " + clazz.getSimpleName() + " is not using the 2- or 3-argument " + "View constructors; XML attributes will not work"); myLogger.warning("wrongconstructor", //$NON-NLS-1$ String.format( "Custom view %1$s is not using the 2- or 3-argument View constructors; XML attributes will not work", clazz.getSimpleName()), null /*data*/); } break; } } catch (NoSuchMethodException ignored) { } } if (constructor == null) { throw e; } } constructor.setAccessible(true); return constructor.newInstance(constructorParameters); } @Nullable private static String getRClassName(@NotNull final Module module) { return ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Nullable @Override public String compute() { final AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { return null; } final Manifest manifest = facet.getManifest(); if (manifest == null) { return null; } final String packageName = manifest.getPackage().getValue(); return packageName == null ? null : packageName + '.' + R_CLASS; } }); } /** * Load and parse the R class such that resource references in the layout rendering can refer * to local resources properly */ public void loadAndParseRClassSilently() { final String rClassName = getRClassName(myModule); try { if (rClassName == null) { LOG.info(String.format("loadAndParseRClass: failed to find manifest package for project %1$s", myModule.getProject().getName())); return; } myLogger.setResourceClass(rClassName); loadAndParseRClass(rClassName); } catch (ClassNotFoundException e) { myLogger.setMissingResourceClass(true); } catch (InconvertibleClassError e) { assert rClassName != null; myLogger.addIncorrectFormatClass(rClassName, e); } } public void loadAndParseRClass(@NotNull String className) throws ClassNotFoundException, InconvertibleClassError { Class<?> aClass = myLoadedClasses.get(className); if (aClass == null) { aClass = getProjectClassLoader().loadClass(className); if (aClass != null) { myLoadedClasses.put(className, aClass); myLogger.setHasLoadedClasses(true); } } if (aClass != null) { final Map<ResourceType, TObjectIntHashMap<String>> res2id = new EnumMap<ResourceType, TObjectIntHashMap<String>>(ResourceType.class); final TIntObjectHashMap<Pair<ResourceType, String>> id2res = new TIntObjectHashMap<Pair<ResourceType, String>>(); final Map<IntArrayWrapper, String> styleableId2res = new HashMap<IntArrayWrapper, String>(); if (parseClass(aClass, id2res, styleableId2res, res2id)) { AppResourceRepository appResources = AppResourceRepository.getAppResources(myModule, true); if (appResources != null) { appResources.setCompiledResources(id2res, styleableId2res, res2id); } } } } private static boolean parseClass(Class<?> rClass, TIntObjectHashMap<Pair<ResourceType, String>> id2res, Map<IntArrayWrapper, String> styleableId2Res, Map<ResourceType, TObjectIntHashMap<String>> res2id) throws ClassNotFoundException { try { final Class<?>[] nestedClasses; try { nestedClasses = rClass.getDeclaredClasses(); } catch (LinkageError e) { final Throwable cause = e.getCause(); if (cause instanceof ClassNotFoundException) { LOG.debug(e); throw (ClassNotFoundException)cause; } throw e; } for (Class<?> resClass : nestedClasses) { final ResourceType resType = ResourceType.getEnum(resClass.getSimpleName()); if (resType != null) { final TObjectIntHashMap<String> resName2Id = new TObjectIntHashMap<String>(); res2id.put(resType, resName2Id); for (Field field : resClass.getDeclaredFields()) { final int modifiers = field.getModifiers(); if (Modifier.isStatic(modifiers)) { // May not be final in library projects final Class<?> type = field.getType(); if (type.isArray() && type.getComponentType() == int.class) { styleableId2Res.put(new IntArrayWrapper((int[])field.get(null)), field.getName()); } else if (type == int.class) { final Integer value = (Integer)field.get(null); id2res.put(value, Pair.of(resType, field.getName())); resName2Id.put(field.getName(), value); } else { LOG.error("Unknown field type in R class: " + type); } } } } } } catch (IllegalAccessException e) { LOG.info(e); return false; } return true; } }