diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk')
11 files changed, 5092 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java new file mode 100755 index 000000000..2396a4c46 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.utils.ILogger; + +/** + * An {@link ILogger} logger that outputs to the ADT console. + */ +public class AdtConsoleSdkLog implements ILogger { + + private static final String TAG = "SDK Manager"; //$NON-NLS-1$ + + @Override + public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { + if (t != null) { + AdtPlugin.logAndPrintError(t, TAG, "Error: " + errorFormat, args); + } else { + AdtPlugin.printErrorToConsole(TAG, String.format(errorFormat, args)); + } + } + + @Override + public void info(@NonNull String msgFormat, Object... args) { + String msg = String.format(msgFormat, args); + for (String s : msg.split("\n")) { + if (s.trim().length() > 0) { + AdtPlugin.printToConsole(TAG, s); + } + } + } + + @Override + public void verbose(@NonNull String msgFormat, Object... args) { + info(msgFormat, args); + } + + @Override + public void warning(@NonNull String warningFormat, Object... args) { + AdtPlugin.printToConsole(TAG, String.format("Warning: " + warningFormat, args)); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtManifestMergeCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtManifestMergeCallback.java new file mode 100755 index 000000000..dc539dcaa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtManifestMergeCallback.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.annotations.NonNull; +import com.android.manifmerger.ICallback; +import com.android.manifmerger.ManifestMerger; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; + +/** + * A {@link ManifestMerger} {@link ICallback} that returns the + * proper API level for known API codenames. + */ +public class AdtManifestMergeCallback implements ICallback { + @Override + public int queryCodenameApiLevel(@NonNull String codename) { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + try { + AndroidVersion version = new AndroidVersion(codename); + String hashString = AndroidTargetHash.getPlatformHashString(version); + IAndroidTarget t = sdk.getTargetFromHashString(hashString); + if (t != null) { + return t.getVersion().getApiLevel(); + } + } catch (AndroidVersion.AndroidVersionException ignore) {} + } + return ICallback.UNKNOWN_CODENAME; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java new file mode 100644 index 000000000..754cedf79 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.SdkConstants; +import com.google.common.io.Closeables; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.management.InvalidAttributeValueException; + +/** + * Custom class loader able to load a class from the SDK jar file. + */ +public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader { + + /** + * Wrapper around a {@link Class} to provide the methods of + * {@link IAndroidClassLoader.IClassDescriptor}. + */ + public final static class ClassWrapper implements IClassDescriptor { + private Class<?> mClass; + + public ClassWrapper(Class<?> clazz) { + mClass = clazz; + } + + @Override + public String getFullClassName() { + return mClass.getCanonicalName(); + } + + @Override + public IClassDescriptor[] getDeclaredClasses() { + Class<?>[] classes = mClass.getDeclaredClasses(); + IClassDescriptor[] iclasses = new IClassDescriptor[classes.length]; + for (int i = 0 ; i < classes.length ; i++) { + iclasses[i] = new ClassWrapper(classes[i]); + } + + return iclasses; + } + + @Override + public IClassDescriptor getEnclosingClass() { + return new ClassWrapper(mClass.getEnclosingClass()); + } + + @Override + public String getSimpleName() { + return mClass.getSimpleName(); + } + + @Override + public IClassDescriptor getSuperclass() { + return new ClassWrapper(mClass.getSuperclass()); + } + + @Override + public boolean equals(Object clazz) { + if (clazz instanceof ClassWrapper) { + return mClass.equals(((ClassWrapper)clazz).mClass); + } + return super.equals(clazz); + } + + @Override + public int hashCode() { + return mClass.hashCode(); + } + + + @Override + public boolean isInstantiable() { + int modifiers = mClass.getModifiers(); + return Modifier.isAbstract(modifiers) == false && Modifier.isPublic(modifiers) == true; + } + + public Class<?> wrappedClass() { + return mClass; + } + + } + + private String mOsFrameworkLocation; + + /** A cache for binary data extracted from the zip */ + private final HashMap<String, byte[]> mEntryCache = new HashMap<String, byte[]>(); + /** A cache for already defined Classes */ + private final HashMap<String, Class<?> > mClassCache = new HashMap<String, Class<?> >(); + + /** + * Creates the class loader by providing the os path to the framework jar archive + * + * @param osFrameworkLocation OS Path of the framework JAR file + */ + public AndroidJarLoader(String osFrameworkLocation) { + super(); + mOsFrameworkLocation = osFrameworkLocation; + } + + @Override + public String getSource() { + return mOsFrameworkLocation; + } + + /** + * Pre-loads all class binary data that belong to the given package by reading the archive + * once and caching them internally. + * <p/> + * This does not actually preload "classes", it just reads the unzipped bytes for a given + * class. To obtain a class, one must call {@link #findClass(String)} later. + * <p/> + * All classes which package name starts with "packageFilter" will be included and can be + * found later. + * <p/> + * May throw some exceptions if the framework JAR cannot be read. + * + * @param packageFilter The package that contains all the class data to preload, using a fully + * qualified binary name (.e.g "com.my.package."). The matching algorithm + * is simple "startsWith". Use an empty string to include everything. + * @param taskLabel An optional task name for the sub monitor. Can be null. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public void preLoadClasses(String packageFilter, String taskLabel, IProgressMonitor monitor) + throws IOException, InvalidAttributeValueException, ClassFormatError { + // Transform the package name into a zip entry path + String pathFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + + SubMonitor progress = SubMonitor.convert(monitor, taskLabel == null ? "" : taskLabel, 100); + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry. + String entryPath = entry.getName(); + + if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) { + // only accept class files + continue; + } + + // check if it is part of the package to preload + if (pathFilter.length() > 0 && !entryPath.startsWith(pathFilter)) { + continue; + } + String className = entryPathToClassName(entryPath); + + if (!mEntryCache.containsKey(className)) { + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + byte[] data = readZipData(zis, (int)entrySize); + mEntryCache.put(className, data); + } + + // advance 5% of whatever is allocated on the progress bar + progress.setWorkRemaining(100); + progress.worked(5); + progress.subTask(String.format("Preload %1$s", className)); + } + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * <p/> + * As a side-effect this will load and cache most, if not all, classes in the input JAR file. + * + * @param packageFilter Base name of package of classes to find. + * Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + @SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly + @Override + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( + String packageFilter, + String[] superClasses) + throws IOException, InvalidAttributeValueException, ClassFormatError { + + packageFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + + HashMap<String, ArrayList<IClassDescriptor>> mClassesFound = + new HashMap<String, ArrayList<IClassDescriptor>>(); + + for (String className : superClasses) { + mClassesFound.put(className, new ArrayList<IClassDescriptor>()); + } + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + try { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry and convert to a class binary name + String entryPath = entry.getName(); + if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) { + // only accept class files + continue; + } + if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) { + // only accept stuff from the requested root package. + continue; + } + String className = entryPathToClassName(entryPath); + + Class<?> loaded_class = mClassCache.get(className); + if (loaded_class == null) { + byte[] data = mEntryCache.get(className); + if (data == null) { + // Get the class and cache it + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + data = readZipData(zis, (int)entrySize); + } + try { + loaded_class = defineAndCacheClass(className, data); + } catch (NoClassDefFoundError error) { + if (error.getMessage().startsWith("java/")) { + // Can't define these; we just need to stop + // iteration here + continue; + } + throw error; + } + } + + for (Class<?> superClass = loaded_class.getSuperclass(); + superClass != null; + superClass = superClass.getSuperclass()) { + String superName = superClass.getCanonicalName(); + if (mClassesFound.containsKey(superName)) { + mClassesFound.get(superName).add(new ClassWrapper(loaded_class)); + break; + } + } + } + } finally { + Closeables.closeQuietly(zis); + } + + return mClassesFound; + } + + /** Helper method that converts a Zip entry path into a corresponding + * Java full qualified binary class name. + * <p/> + * F.ex, this converts "com/my/package/Foo.class" into "com.my.package.Foo". + */ + private String entryPathToClassName(String entryPath) { + return entryPath.replaceFirst("\\.class$", "").replaceAll("[/\\\\]", "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Finds the class with the specified binary name. + * + * {@inheritDoc} + */ + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + try { + // try to find the class in the cache + Class<?> cached_class = mClassCache.get(name); + if (cached_class == ClassNotFoundException.class) { + // we already know we can't find this class, don't try again + throw new ClassNotFoundException(name); + } else if (cached_class != null) { + return cached_class; + } + + // if not found, look it up and cache it + byte[] data = loadClassData(name); + if (data != null) { + return defineAndCacheClass(name, data); + } else { + // if the class can't be found, record a CNFE class in the map so + // that we don't try to reload it next time + mClassCache.put(name, ClassNotFoundException.class); + throw new ClassNotFoundException(name); + } + } catch (ClassNotFoundException e) { + throw e; + } catch (Exception e) { + throw new ClassNotFoundException(e.getMessage()); + } + } + + /** + * Defines a class based on its binary data and caches the resulting class object. + * + * @param name The binary name of the class (i.e. package.class1$class2) + * @param data The binary data from the loader. + * @return The class defined + * @throws ClassFormatError if defineClass failed. + */ + private Class<?> defineAndCacheClass(String name, byte[] data) throws ClassFormatError { + Class<?> cached_class; + cached_class = defineClass(null, data, 0, data.length); + + if (cached_class != null) { + // Add new class to the cache class and remove it from the zip entry data cache + mClassCache.put(name, cached_class); + mEntryCache.remove(name); + } + return cached_class; + } + + /** + * Loads a class data from its binary name. + * <p/> + * This uses the class binary data that has been preloaded earlier by the preLoadClasses() + * method if possible. + * + * @param className the binary name + * @return an array of bytes representing the class data or null if not found + * @throws InvalidAttributeValueException + * @throws IOException + */ + private synchronized byte[] loadClassData(String className) + throws InvalidAttributeValueException, IOException { + + byte[] data = mEntryCache.get(className); + if (data != null) { + return data; + } + + // The name is a binary name. Something like "android.R", or "android.R$id". + // Make a path out of it. + String entryName = className.replaceAll("\\.", "/") + SdkConstants.DOT_CLASS; //$NON-NLS-1$ //$NON-NLS-2$ + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + try { + // loop on the entries of the intermediary package and put them in the final package. + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry. + String currEntryName = entry.getName(); + + if (currEntryName.equals(entryName)) { + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + + data = readZipData(zis, (int)entrySize); + return data; + } + } + + return null; + } finally { + zis.close(); + } + } + + /** + * Reads data for the <em>current</em> entry from the zip input stream. + * + * @param zis The Zip input stream + * @param entrySize The entry size. -1 if unknown. + * @return The new data for the <em>current</em> entry. + * @throws IOException If ZipInputStream.read() fails. + */ + private byte[] readZipData(ZipInputStream zis, int entrySize) throws IOException { + int block_size = 1024; + int data_size = entrySize < 1 ? block_size : entrySize; + int offset = 0; + byte[] data = new byte[data_size]; + + while(zis.available() != 0) { + int count = zis.read(data, offset, data_size - offset); + if (count < 0) { // read data is done + break; + } + offset += count; + + if (entrySize >= 1 && offset >= entrySize) { // we know the size and we're done + break; + } + + // if we don't know the entry size and we're not done reading, + // expand the data buffer some more. + if (offset >= data_size) { + byte[] temp = new byte[data_size + block_size]; + System.arraycopy(data, 0, temp, 0, data_size); + data_size += block_size; + data = temp; + block_size *= 2; + } + } + + if (offset < data_size) { + // buffer was allocated too large, trim it + byte[] temp = new byte[offset]; + if (offset > 0) { + System.arraycopy(data, 0, temp, 0, offset); + } + data = temp; + } + + return data; + } + + /** + * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + @Override + public IClassDescriptor getClass(String className) throws ClassNotFoundException { + try { + return new ClassWrapper(loadClass(className)); + } catch (ClassNotFoundException e) { + throw e; // useful for debugging + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetData.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetData.java new file mode 100644 index 000000000..85ae9fdc0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetData.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.animator.AnimDescriptors; +import com.android.ide.eclipse.adt.internal.editors.animator.AnimatorDescriptors; +import com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors; +import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.adt.internal.editors.drawable.DrawableDescriptors; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.menu.descriptors.MenuDescriptors; +import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors; +import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; + +import org.eclipse.core.runtime.IStatus; + +import java.io.File; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Map; + +/** + * This class contains the data of an Android Target as loaded from the SDK. + */ +public class AndroidTargetData { + + public final static int DESCRIPTOR_MANIFEST = 1; + public final static int DESCRIPTOR_LAYOUT = 2; + public final static int DESCRIPTOR_MENU = 3; + public final static int DESCRIPTOR_OTHER_XML = 4; + public final static int DESCRIPTOR_RESOURCES = 5; + public final static int DESCRIPTOR_SEARCHABLE = 6; + public final static int DESCRIPTOR_PREFERENCES = 7; + public final static int DESCRIPTOR_APPWIDGET_PROVIDER = 8; + public final static int DESCRIPTOR_DRAWABLE = 9; + public final static int DESCRIPTOR_ANIMATOR = 10; + public final static int DESCRIPTOR_ANIM = 11; + public final static int DESCRIPTOR_COLOR = 12; + + private final IAndroidTarget mTarget; + + /** + * mAttributeValues is a map { key => list [ values ] }. + * The key for the map is "(element-xml-name,attribute-namespace:attribute-xml-local-name)". + * The attribute namespace prefix must be: + * - "android" for SdkConstants.NS_RESOURCES + * - "xmlns" for the XMLNS URI. + * + * This is used for attributes that do not have a unique name, but still need to be populated + * with values in the UI. Uniquely named attributes have their values in {@link #mEnumValueMap}. + */ + private Hashtable<String, String[]> mAttributeValues = new Hashtable<String, String[]>(); + + private AndroidManifestDescriptors mManifestDescriptors; + private DrawableDescriptors mDrawableDescriptors; + private AnimatorDescriptors mAnimatorDescriptors; + private AnimDescriptors mAnimDescriptors; + private ColorDescriptors mColorDescriptors; + private LayoutDescriptors mLayoutDescriptors; + private MenuDescriptors mMenuDescriptors; + private OtherXmlDescriptors mOtherXmlDescriptors; + + private Map<String, Map<String, Integer>> mEnumValueMap; + + private ResourceRepository mFrameworkResources; + private LayoutLibrary mLayoutLibrary; + private Map<String, AttributeInfo> mAttributeMap; + + private boolean mLayoutBridgeInit = false; + + AndroidTargetData(IAndroidTarget androidTarget) { + mTarget = androidTarget; + } + + /** + * Sets the associated map from string attribute name to + * {@link AttributeInfo} + * + * @param attributeMap the map + */ + public void setAttributeMap(@NonNull Map<String, AttributeInfo> attributeMap) { + mAttributeMap = attributeMap; + } + + /** + * Returns the associated map from string attribute name to + * {@link AttributeInfo} + * + * @return the map + */ + @Nullable + public Map<String, AttributeInfo> getAttributeMap() { + return mAttributeMap; + } + + /** + * Creates an AndroidTargetData object. + */ + void setExtraData( + AndroidManifestDescriptors manifestDescriptors, + LayoutDescriptors layoutDescriptors, + MenuDescriptors menuDescriptors, + OtherXmlDescriptors otherXmlDescriptors, + DrawableDescriptors drawableDescriptors, + AnimatorDescriptors animatorDescriptors, + AnimDescriptors animDescriptors, + ColorDescriptors colorDescriptors, + Map<String, Map<String, Integer>> enumValueMap, + String[] permissionValues, + String[] activityIntentActionValues, + String[] broadcastIntentActionValues, + String[] serviceIntentActionValues, + String[] intentCategoryValues, + String[] platformLibraries, + IOptionalLibrary[] optionalLibraries, + ResourceRepository frameworkResources, + LayoutLibrary layoutLibrary) { + + mManifestDescriptors = manifestDescriptors; + mDrawableDescriptors = drawableDescriptors; + mAnimatorDescriptors = animatorDescriptors; + mAnimDescriptors = animDescriptors; + mColorDescriptors = colorDescriptors; + mLayoutDescriptors = layoutDescriptors; + mMenuDescriptors = menuDescriptors; + mOtherXmlDescriptors = otherXmlDescriptors; + mEnumValueMap = enumValueMap; + mFrameworkResources = frameworkResources; + mLayoutLibrary = layoutLibrary; + + setPermissions(permissionValues); + setIntentFilterActionsAndCategories(activityIntentActionValues, broadcastIntentActionValues, + serviceIntentActionValues, intentCategoryValues); + setOptionalLibraries(platformLibraries, optionalLibraries); + } + + /** + * Returns an {@link IDescriptorProvider} from a given Id. + * The Id can be one of {@link #DESCRIPTOR_MANIFEST}, {@link #DESCRIPTOR_LAYOUT}, + * {@link #DESCRIPTOR_MENU}, or {@link #DESCRIPTOR_OTHER_XML}. + * All other values will throw an {@link IllegalArgumentException}. + */ + public IDescriptorProvider getDescriptorProvider(int descriptorId) { + switch (descriptorId) { + case DESCRIPTOR_MANIFEST: + return mManifestDescriptors; + case DESCRIPTOR_LAYOUT: + return mLayoutDescriptors; + case DESCRIPTOR_MENU: + return mMenuDescriptors; + case DESCRIPTOR_OTHER_XML: + return mOtherXmlDescriptors; + case DESCRIPTOR_RESOURCES: + // FIXME: since it's hard-coded the Resources Descriptors are not platform dependent. + return ValuesDescriptors.getInstance(); + case DESCRIPTOR_PREFERENCES: + return mOtherXmlDescriptors.getPreferencesProvider(); + case DESCRIPTOR_APPWIDGET_PROVIDER: + return mOtherXmlDescriptors.getAppWidgetProvider(); + case DESCRIPTOR_SEARCHABLE: + return mOtherXmlDescriptors.getSearchableProvider(); + case DESCRIPTOR_DRAWABLE: + return mDrawableDescriptors; + case DESCRIPTOR_ANIMATOR: + return mAnimatorDescriptors; + case DESCRIPTOR_ANIM: + return mAnimDescriptors; + case DESCRIPTOR_COLOR: + return mColorDescriptors; + default : + throw new IllegalArgumentException(); + } + } + + /** + * Returns the manifest descriptors. + */ + public AndroidManifestDescriptors getManifestDescriptors() { + return mManifestDescriptors; + } + + /** + * Returns the drawable descriptors + */ + public DrawableDescriptors getDrawableDescriptors() { + return mDrawableDescriptors; + } + + /** + * Returns the animation descriptors + */ + public AnimDescriptors getAnimDescriptors() { + return mAnimDescriptors; + } + + /** + * Returns the color descriptors + */ + public ColorDescriptors getColorDescriptors() { + return mColorDescriptors; + } + + /** + * Returns the animator descriptors + */ + public AnimatorDescriptors getAnimatorDescriptors() { + return mAnimatorDescriptors; + } + + /** + * Returns the layout Descriptors. + */ + public LayoutDescriptors getLayoutDescriptors() { + return mLayoutDescriptors; + } + + /** + * Returns the menu descriptors. + */ + public MenuDescriptors getMenuDescriptors() { + return mMenuDescriptors; + } + + /** + * Returns the XML descriptors + */ + public OtherXmlDescriptors getXmlDescriptors() { + return mOtherXmlDescriptors; + } + + /** + * Returns this list of possible values for an XML attribute. + * <p/>This should only be called for attributes for which possible values depend on the + * parent element node. + * <p/>For attributes that have the same values no matter the parent node, use + * {@link #getEnumValueMap()}. + * @param elementName the name of the element containing the attribute. + * @param attributeName the name of the attribute + * @return an array of String with the possible values, or <code>null</code> if no values were + * found. + */ + public String[] getAttributeValues(String elementName, String attributeName) { + String key = String.format("(%1$s,%2$s)", elementName, attributeName); //$NON-NLS-1$ + return mAttributeValues.get(key); + } + + /** + * Returns this list of possible values for an XML attribute. + * <p/>This should only be called for attributes for which possible values depend on the + * parent and great-grand-parent element node. + * <p/>The typical example of this is for the 'name' attribute under + * activity/intent-filter/action + * <p/>For attributes that have the same values no matter the parent node, use + * {@link #getEnumValueMap()}. + * @param elementName the name of the element containing the attribute. + * @param attributeName the name of the attribute + * @param greatGrandParentElementName the great-grand-parent node. + * @return an array of String with the possible values, or <code>null</code> if no values were + * found. + */ + public String[] getAttributeValues(String elementName, String attributeName, + String greatGrandParentElementName) { + if (greatGrandParentElementName != null) { + String key = String.format("(%1$s,%2$s,%3$s)", //$NON-NLS-1$ + greatGrandParentElementName, elementName, attributeName); + String[] values = mAttributeValues.get(key); + if (values != null) { + return values; + } + } + + return getAttributeValues(elementName, attributeName); + } + + /** + * Returns the enum values map. + * <p/>The map defines the possible values for XML attributes. The key is the attribute name + * and the value is a map of (string, integer) in which the key (string) is the name of + * the value, and the Integer is the numerical value in the compiled binary XML files. + */ + public Map<String, Map<String, Integer>> getEnumValueMap() { + return mEnumValueMap; + } + + /** + * Returns the {@link ProjectResources} containing the Framework Resources. + */ + public ResourceRepository getFrameworkResources() { + return mFrameworkResources; + } + + /** + * Returns a {@link LayoutLibrary} object possibly containing a {@link LayoutBridge} object. + * <p/>If {@link LayoutLibrary#getBridge()} is <code>null</code>, + * {@link LayoutBridge#getStatus()} will contain the reason (either {@link LoadStatus#LOADING} + * or {@link LoadStatus#FAILED}). + * <p/>Valid {@link LayoutBridge} objects are always initialized before being returned. + */ + public synchronized LayoutLibrary getLayoutLibrary() { + if (mLayoutBridgeInit == false && mLayoutLibrary.getStatus() == LoadStatus.LOADED) { + boolean ok = mLayoutLibrary.init( + mTarget.getProperties(), + new File(mTarget.getPath(IAndroidTarget.FONTS)), + getEnumValueMap(), + new LayoutLog() { + + @Override + public void error(String tag, String message, Throwable throwable, + Object data) { + AdtPlugin.log(throwable, message); + } + + @Override + public void error(String tag, String message, Object data) { + AdtPlugin.log(IStatus.ERROR, message); + } + + @Override + public void warning(String tag, String message, Object data) { + AdtPlugin.log(IStatus.WARNING, message); + } + }); + if (!ok) { + AdtPlugin.log(IStatus.ERROR, + "LayoutLibrary initialization failed"); + } + mLayoutBridgeInit = true; + } + + return mLayoutLibrary; + } + + /** + * Sets the permission values + * @param permissionValues the list of permissions + */ + private void setPermissions(String[] permissionValues) { + setValues("(uses-permission,android:name)", permissionValues); //$NON-NLS-1$ + setValues("(application,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(activity,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(receiver,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(service,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(provider,android:permission)", permissionValues); //$NON-NLS-1$ + } + + private void setIntentFilterActionsAndCategories(String[] activityIntentActions, + String[] broadcastIntentActions, String[] serviceIntentActions, + String[] intentCategoryValues) { + setValues("(activity,action,android:name)", activityIntentActions); //$NON-NLS-1$ + setValues("(receiver,action,android:name)", broadcastIntentActions); //$NON-NLS-1$ + setValues("(service,action,android:name)", serviceIntentActions); //$NON-NLS-1$ + setValues("(category,android:name)", intentCategoryValues); //$NON-NLS-1$ + } + + private void setOptionalLibraries(String[] platformLibraries, + IOptionalLibrary[] optionalLibraries) { + + ArrayList<String> libs = new ArrayList<String>(); + + if (platformLibraries != null) { + for (String name : platformLibraries) { + libs.add(name); + } + } + + if (optionalLibraries != null) { + for (int i = 0; i < optionalLibraries.length; i++) { + libs.add(optionalLibraries[i].getName()); + } + } + setValues("(uses-library,android:name)", libs.toArray(new String[libs.size()])); + } + + /** + * Sets a (name, values) pair in the hash map. + * <p/> + * If the name is already present in the map, it is first removed. + * @param name the name associated with the values. + * @param values The values to add. + */ + private void setValues(String name, String[] values) { + mAttributeValues.remove(name); + mAttributeValues.put(name, values); + } + + public void dispose() { + if (mLayoutLibrary != null) { + mLayoutLibrary.dispose(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetParser.java new file mode 100644 index 000000000..9a1fd3dc9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidTargetParser.java @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.SdkConstants; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.platform.AttrsXmlParser; +import com.android.ide.common.resources.platform.DeclareStyleableInfo; +import com.android.ide.common.resources.platform.ViewClassInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.animator.AnimDescriptors; +import com.android.ide.eclipse.adt.internal.editors.animator.AnimatorDescriptors; +import com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors; +import com.android.ide.eclipse.adt.internal.editors.drawable.DrawableDescriptors; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.menu.descriptors.MenuDescriptors; +import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.InvalidAttributeValueException; + +/** + * Parser for the platform data in an SDK. + * <p/> + * This gather the following information: + * <ul> + * <li>Resource ID from <code>android.R</code></li> + * <li>The list of permissions values from <code>android.Manifest$permission</code></li> + * <li></li> + * </ul> + */ +public final class AndroidTargetParser { + + private static final String TAG = "Framework Resource Parser"; + private final IAndroidTarget mAndroidTarget; + + /** + * Creates a platform data parser. + */ + public AndroidTargetParser(IAndroidTarget platformTarget) { + mAndroidTarget = platformTarget; + } + + /** + * Parses the framework, collects all interesting information and stores them in the + * {@link IAndroidTarget} given to the constructor. + * + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + * @return True if the SDK path was valid and parsing has been attempted. + */ + public IStatus run(IProgressMonitor monitor) { + try { + SubMonitor progress = SubMonitor.convert(monitor, + String.format("Parsing SDK %1$s", mAndroidTarget.getName()), + 16); + + AndroidTargetData targetData = new AndroidTargetData(mAndroidTarget); + + // parse the rest of the data. + + AndroidJarLoader classLoader = + new AndroidJarLoader(mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + preload(classLoader, progress.newChild(40, SubMonitor.SUPPRESS_NONE)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // get the permissions + progress.subTask("Permissions"); + String[] permissionValues = collectPermissions(classLoader); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // get the action and category values for the Intents. + progress.subTask("Intents"); + ArrayList<String> activity_actions = new ArrayList<String>(); + ArrayList<String> broadcast_actions = new ArrayList<String>(); + ArrayList<String> service_actions = new ArrayList<String>(); + ArrayList<String> categories = new ArrayList<String>(); + collectIntentFilterActionsAndCategories(activity_actions, broadcast_actions, + service_actions, categories); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // gather the attribute definition + progress.subTask("Attributes definitions"); + AttrsXmlParser attrsXmlParser = new AttrsXmlParser( + mAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES), + AdtPlugin.getDefault(), + 1000); + attrsXmlParser.preload(); + + progress.worked(1); + + progress.subTask("Manifest definitions"); + AttrsXmlParser attrsManifestXmlParser = new AttrsXmlParser( + mAndroidTarget.getPath(IAndroidTarget.MANIFEST_ATTRIBUTES), + attrsXmlParser, + AdtPlugin.getDefault(), 1100); + attrsManifestXmlParser.preload(); + progress.worked(1); + + Collection<ViewClassInfo> mainList = new ArrayList<ViewClassInfo>(); + Collection<ViewClassInfo> groupList = new ArrayList<ViewClassInfo>(); + + // collect the layout/widgets classes + progress.subTask("Widgets and layouts"); + collectLayoutClasses(classLoader, attrsXmlParser, mainList, groupList, + progress.newChild(1)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ViewClassInfo[] layoutViewsInfo = mainList.toArray( + new ViewClassInfo[mainList.size()]); + ViewClassInfo[] layoutGroupsInfo = groupList.toArray( + new ViewClassInfo[groupList.size()]); + mainList.clear(); + groupList.clear(); + + // collect the preferences classes. + collectPreferenceClasses(classLoader, attrsXmlParser, mainList, groupList, + progress.newChild(1)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ViewClassInfo[] preferencesInfo = mainList.toArray(new ViewClassInfo[mainList.size()]); + ViewClassInfo[] preferenceGroupsInfo = groupList.toArray( + new ViewClassInfo[groupList.size()]); + + Map<String, DeclareStyleableInfo> xmlMenuMap = collectMenuDefinitions(attrsXmlParser); + Map<String, DeclareStyleableInfo> xmlSearchableMap = collectSearchableDefinitions( + attrsXmlParser); + Map<String, DeclareStyleableInfo> manifestMap = collectManifestDefinitions( + attrsManifestXmlParser); + Map<String, Map<String, Integer>> enumValueMap = attrsXmlParser.getEnumFlagValues(); + + Map<String, DeclareStyleableInfo> xmlAppWidgetMap = null; + if (mAndroidTarget.getVersion().getApiLevel() >= 3) { + xmlAppWidgetMap = collectAppWidgetDefinitions(attrsXmlParser); + } + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // From the information that was collected, create the pieces that will be put in + // the PlatformData object. + AndroidManifestDescriptors manifestDescriptors = new AndroidManifestDescriptors(); + manifestDescriptors.updateDescriptors(manifestMap); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + LayoutDescriptors layoutDescriptors = new LayoutDescriptors(); + layoutDescriptors.updateDescriptors(layoutViewsInfo, layoutGroupsInfo, + attrsXmlParser.getDeclareStyleableList(), mAndroidTarget); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + MenuDescriptors menuDescriptors = new MenuDescriptors(); + menuDescriptors.updateDescriptors(xmlMenuMap); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + OtherXmlDescriptors otherXmlDescriptors = new OtherXmlDescriptors(); + otherXmlDescriptors.updateDescriptors( + xmlSearchableMap, + xmlAppWidgetMap, + preferencesInfo, + preferenceGroupsInfo); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + DrawableDescriptors drawableDescriptors = new DrawableDescriptors(); + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + drawableDescriptors.updateDescriptors(map); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + AnimatorDescriptors animatorDescriptors = new AnimatorDescriptors(); + animatorDescriptors.updateDescriptors(map); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + AnimDescriptors animDescriptors = new AnimDescriptors(); + animDescriptors.updateDescriptors(map); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ColorDescriptors colorDescriptors = new ColorDescriptors(); + colorDescriptors.updateDescriptors(map); + progress.worked(1); + + // load the framework resources. + ResourceRepository frameworkResources = + ResourceManager.getInstance().loadFrameworkResources(mAndroidTarget); + progress.worked(1); + + // now load the layout lib bridge + LayoutLibrary layoutBridge = LayoutLibrary.load( + mAndroidTarget.getPath(IAndroidTarget.LAYOUT_LIB), + AdtPlugin.getDefault(), + "ADT plug-in"); + + progress.worked(1); + + // and finally create the PlatformData with all that we loaded. + targetData.setExtraData( + manifestDescriptors, + layoutDescriptors, + menuDescriptors, + otherXmlDescriptors, + drawableDescriptors, + animatorDescriptors, + animDescriptors, + colorDescriptors, + enumValueMap, + permissionValues, + activity_actions.toArray(new String[activity_actions.size()]), + broadcast_actions.toArray(new String[broadcast_actions.size()]), + service_actions.toArray(new String[service_actions.size()]), + categories.toArray(new String[categories.size()]), + mAndroidTarget.getPlatformLibraries(), + mAndroidTarget.getOptionalLibraries(), + frameworkResources, + layoutBridge); + + targetData.setAttributeMap(attrsXmlParser.getAttributeMap()); + + Sdk.getCurrent().setTargetData(mAndroidTarget, targetData); + + return Status.OK_STATUS; + } catch (Exception e) { + AdtPlugin.logAndPrintError(e, TAG, "SDK parser failed"); //$NON-NLS-1$ + AdtPlugin.printToConsole("SDK parser failed", e.getMessage()); + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "SDK parser failed", e); + } + } + + /** + * Preloads all "interesting" classes from the framework SDK jar. + * <p/> + * Currently this preloads all classes from the framework jar + * + * @param classLoader The framework SDK jar classloader + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void preload(AndroidJarLoader classLoader, IProgressMonitor monitor) { + try { + classLoader.preLoadClasses("" /* all classes */, //$NON-NLS-1$ + mAndroidTarget.getName(), // monitor task label + monitor); + } catch (InvalidAttributeValueException e) { + AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$ + } catch (IOException e) { + AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$ + } + } + + /** + * Loads, collects and returns the list of default permissions from the framework. + * + * @param classLoader The framework SDK jar classloader + * @return a non null (but possibly empty) array containing the permission values. + */ + private String[] collectPermissions(AndroidJarLoader classLoader) { + try { + Class<?> permissionClass = + classLoader.loadClass(SdkConstants.CLASS_MANIFEST_PERMISSION); + + if (permissionClass != null) { + ArrayList<String> list = new ArrayList<String>(); + + Field[] fields = permissionClass.getFields(); + + for (Field f : fields) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) && + Modifier.isPublic(modifiers)) { + try { + Object value = f.get(null); + if (value instanceof String) { + list.add((String)value); + } + } catch (IllegalArgumentException e) { + // since we provide null this should not happen + } catch (IllegalAccessException e) { + // if the field is inaccessible we ignore it. + } catch (NullPointerException npe) { + // looks like this is not a static field. we can ignore. + } catch (ExceptionInInitializerError eiie) { + // lets just ignore the field again + } + } + } + + return list.toArray(new String[list.size()]); + } + } catch (ClassNotFoundException e) { + AdtPlugin.logAndPrintError(e, TAG, + "Collect permissions failed, class %1$s not found in %2$s", //$NON-NLS-1$ + SdkConstants.CLASS_MANIFEST_PERMISSION, + mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); + } + + return new String[0]; + } + + /** + * Loads and collects the action and category default values from the framework. + * The values are added to the <code>actions</code> and <code>categories</code> lists. + * + * @param activityActions the list which will receive the activity action values. + * @param broadcastActions the list which will receive the broadcast action values. + * @param serviceActions the list which will receive the service action values. + * @param categories the list which will receive the category values. + */ + private void collectIntentFilterActionsAndCategories(ArrayList<String> activityActions, + ArrayList<String> broadcastActions, + ArrayList<String> serviceActions, ArrayList<String> categories) { + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_ACTIVITY), + activityActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_BROADCAST), + broadcastActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_SERVICE), + serviceActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.CATEGORIES), + categories); + } + + /** + * Collects values from a text file located in the SDK + * @param osFilePath The path to the text file. + * @param values the {@link ArrayList} to fill with the values. + */ + private void collectValues(String osFilePath, ArrayList<String> values) { + FileReader fr = null; + BufferedReader reader = null; + try { + fr = new FileReader(osFilePath); + reader = new BufferedReader(fr); + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0 && line.startsWith("#") == false) { //$NON-NLS-1$ + values.add(line); + } + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } + + try { + if (fr != null) { + fr.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } + } + } + + /** + * Collects all layout classes information from the class loader and the + * attrs.xml and sets the corresponding structures in the resource manager. + * + * @param classLoader The framework SDK jar classloader in case we cannot get the widget from + * the platform directly + * @param attrsXmlParser The parser of the attrs.xml file + * @param mainList the Collection to receive the main list of {@link ViewClassInfo}. + * @param groupList the Collection to receive the group list of {@link ViewClassInfo}. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void collectLayoutClasses(AndroidJarLoader classLoader, + AttrsXmlParser attrsXmlParser, + Collection<ViewClassInfo> mainList, + Collection<ViewClassInfo> groupList, + IProgressMonitor monitor) { + LayoutParamsParser ldp = null; + try { + WidgetClassLoader loader = new WidgetClassLoader( + mAndroidTarget.getPath(IAndroidTarget.WIDGETS)); + if (loader.parseWidgetList(monitor)) { + ldp = new LayoutParamsParser(loader, attrsXmlParser); + } + // if the parsing failed, we'll use the old loader below. + } catch (FileNotFoundException e) { + AdtPlugin.log(e, "Android Framework Parser"); //$NON-NLS-1$ + // the file does not exist, we'll use the old loader below. + } + + if (ldp == null) { + ldp = new LayoutParamsParser(classLoader, attrsXmlParser); + } + ldp.parseLayoutClasses(monitor); + + List<ViewClassInfo> views = ldp.getViews(); + List<ViewClassInfo> groups = ldp.getGroups(); + + if (views != null && groups != null) { + mainList.addAll(views); + groupList.addAll(groups); + } + } + + /** + * Collects all preferences definition information from the attrs.xml and + * sets the corresponding structures in the resource manager. + * + * @param classLoader The framework SDK jar classloader + * @param attrsXmlParser The parser of the attrs.xml file + * @param mainList the Collection to receive the main list of {@link ViewClassInfo}. + * @param groupList the Collection to receive the group list of {@link ViewClassInfo}. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void collectPreferenceClasses(AndroidJarLoader classLoader, + AttrsXmlParser attrsXmlParser, Collection<ViewClassInfo> mainList, + Collection<ViewClassInfo> groupList, IProgressMonitor monitor) { + LayoutParamsParser ldp = new LayoutParamsParser(classLoader, attrsXmlParser); + + try { + ldp.parsePreferencesClasses(monitor); + + List<ViewClassInfo> prefs = ldp.getViews(); + List<ViewClassInfo> groups = ldp.getGroups(); + + if (prefs != null && groups != null) { + mainList.addAll(prefs); + groupList.addAll(groups); + } + } catch (NoClassDefFoundError e) { + AdtPlugin.logAndPrintError(e, TAG, + "Collect preferences failed, class %1$s not found in %2$s", + e.getMessage(), + classLoader.getSource()); + } catch (Throwable e) { + AdtPlugin.log(e, "Android Framework Parser: failed to collect preference classes"); //$NON-NLS-1$ + AdtPlugin.printErrorToConsole("Android Framework Parser", + "failed to collect preference classes"); + } + } + + /** + * Collects all menu definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectMenuDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "Menu", //$NON-NLS-1$ + "MenuItem", //$NON-NLS-1$ + "MenuGroup" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all searchable definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectSearchableDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "Searchable", //$NON-NLS-1$ + "SearchableActionKey" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all appWidgetProviderInfo definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectAppWidgetDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "AppWidgetProviderInfo" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "AppWidget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("AppWidget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all manifest definition information from the attrs_manifest.xml and returns it. + */ + private Map<String, DeclareStyleableInfo> collectManifestDefinitions( + AttrsXmlParser attrsXmlParser) { + + return attrsXmlParser.getDeclareStyleableList(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/IAndroidClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/IAndroidClassLoader.java new file mode 100644 index 000000000..ab78d2a9b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/IAndroidClassLoader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.management.InvalidAttributeValueException; + +/** + * Classes which implements this interface provide methods to access framework resource + * data loaded from the SDK. + */ +interface IAndroidClassLoader { + + /** + * Classes which implement this interface provide methods to describe a class. + */ + public interface IClassDescriptor { + + String getFullClassName(); + + IClassDescriptor getSuperclass(); + + String getSimpleName(); + + IClassDescriptor getEnclosingClass(); + + IClassDescriptor[] getDeclaredClasses(); + + boolean isInstantiable(); + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * + * @param rootPackage Root package of classes to find. Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( + String rootPackage, String[] superClasses) + throws IOException, InvalidAttributeValueException, ClassFormatError; + + /** + * Returns a {@link IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + public IClassDescriptor getClass(String className) throws ClassNotFoundException; + + /** + * Returns a string indicating the source of the classes, typically for debugging + * or in error messages. This would typically be a JAR file name or some kind of + * identifier that would mean something to the user when looking at error messages. + * + * @return An informal string representing the source of the classes. + */ + public String getSource(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutParamsParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutParamsParser.java new file mode 100644 index 000000000..d05c12a9e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/LayoutParamsParser.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.SdkConstants; +import com.android.ide.common.resources.platform.AttrsXmlParser; +import com.android.ide.common.resources.platform.ViewClassInfo; +import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.IAndroidClassLoader.IClassDescriptor; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.management.InvalidAttributeValueException; + +/* + * TODO: refactor this. Could use some cleanup. + */ + +/** + * Parser for the framework library. + * <p/> + * This gather the following information: + * <ul> + * <li>Resource ID from <code>android.R</code></li> + * <li>The list of permissions values from <code>android.Manifest$permission</code></li> + * <li></li> + * </ul> + */ +public class LayoutParamsParser { + + /** + * Class extending {@link ViewClassInfo} by adding the notion of instantiability. + * {@link LayoutParamsParser#getViews()} and {@link LayoutParamsParser#getGroups()} should + * only return classes that can be instantiated. + */ + final static class ExtViewClassInfo extends ViewClassInfo { + + private boolean mIsInstantiable; + + ExtViewClassInfo(boolean instantiable, boolean isLayout, String canonicalClassName, + String shortClassName) { + super(isLayout, canonicalClassName, shortClassName); + mIsInstantiable = instantiable; + } + + boolean isInstantiable() { + return mIsInstantiable; + } + } + + /* Note: protected members/methods are overridden in unit tests */ + + /** Reference to android.view.View */ + protected IClassDescriptor mTopViewClass; + /** Reference to android.view.ViewGroup */ + protected IClassDescriptor mTopGroupClass; + /** Reference to android.view.ViewGroup$LayoutParams */ + protected IClassDescriptor mTopLayoutParamsClass; + + /** Input list of all classes deriving from android.view.View */ + protected ArrayList<IClassDescriptor> mViewList; + /** Input list of all classes deriving from android.view.ViewGroup */ + protected ArrayList<IClassDescriptor> mGroupList; + + /** Output map of FQCN => info on View classes */ + protected TreeMap<String, ExtViewClassInfo> mViewMap; + /** Output map of FQCN => info on ViewGroup classes */ + protected TreeMap<String, ExtViewClassInfo> mGroupMap; + /** Output map of FQCN => info on LayoutParams classes */ + protected HashMap<String, LayoutParamsInfo> mLayoutParamsMap; + + /** The attrs.xml parser */ + protected AttrsXmlParser mAttrsXmlParser; + + /** The android.jar class loader */ + protected IAndroidClassLoader mClassLoader; + + /** + * Instantiate a new LayoutParamsParser. + * @param classLoader The android.jar class loader + * @param attrsXmlParser The parser of the attrs.xml file + */ + public LayoutParamsParser(IAndroidClassLoader classLoader, + AttrsXmlParser attrsXmlParser) { + mClassLoader = classLoader; + mAttrsXmlParser = attrsXmlParser; + } + + /** Returns the map of FQCN => info on View classes */ + public List<ViewClassInfo> getViews() { + return getInstantiables(mViewMap); + } + + /** Returns the map of FQCN => info on ViewGroup classes */ + public List<ViewClassInfo> getGroups() { + return getInstantiables(mGroupMap); + } + + /** + * TODO: doc here. + * <p/> + * Note: on output we should have NO dependency on {@link IClassDescriptor}, + * otherwise we wouldn't be able to unload the class loader later. + * <p/> + * Note on Vocabulary: FQCN=Fully Qualified Class Name (e.g. "my.package.class$innerClass") + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + public void parseLayoutClasses(IProgressMonitor monitor) { + parseClasses(monitor, + SdkConstants.CLASS_VIEW, + SdkConstants.CLASS_VIEWGROUP, + SdkConstants.CLASS_VIEWGROUP_LAYOUTPARAMS); + } + + public void parsePreferencesClasses(IProgressMonitor monitor) { + parseClasses(monitor, + SdkConstants.CLASS_PREFERENCE, + SdkConstants.CLASS_PREFERENCEGROUP, + null /* paramsClassName */ ); + } + + private void parseClasses(IProgressMonitor monitor, + String rootClassName, + String groupClassName, + String paramsClassName) { + try { + SubMonitor progress = SubMonitor.convert(monitor, 100); + + String[] superClasses = new String[2 + (paramsClassName == null ? 0 : 1)]; + superClasses[0] = groupClassName; + superClasses[1] = rootClassName; + if (paramsClassName != null) { + superClasses[2] = paramsClassName; + } + HashMap<String, ArrayList<IClassDescriptor>> found = + mClassLoader.findClassesDerivingFrom("android.", superClasses); //$NON-NLS-1$ + mTopViewClass = mClassLoader.getClass(rootClassName); + mTopGroupClass = mClassLoader.getClass(groupClassName); + if (paramsClassName != null) { + mTopLayoutParamsClass = mClassLoader.getClass(paramsClassName); + } + + mViewList = found.get(rootClassName); + mGroupList = found.get(groupClassName); + + mViewMap = new TreeMap<String, ExtViewClassInfo>(); + mGroupMap = new TreeMap<String, ExtViewClassInfo>(); + if (mTopLayoutParamsClass != null) { + mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>(); + } + + // Add top classes to the maps since by design they are not listed in classes deriving + // from themselves. + if (mTopGroupClass != null) { + addGroup(mTopGroupClass); + } + if (mTopViewClass != null) { + addView(mTopViewClass); + } + + // ViewGroup derives from View + ExtViewClassInfo vg = mGroupMap.get(groupClassName); + if (vg != null) { + vg.setSuperClass(mViewMap.get(rootClassName)); + } + + progress.setWorkRemaining(mGroupList.size() + mViewList.size()); + + for (IClassDescriptor groupChild : mGroupList) { + addGroup(groupChild); + progress.worked(1); + } + + for (IClassDescriptor viewChild : mViewList) { + if (viewChild != mTopGroupClass) { + addView(viewChild); + } + progress.worked(1); + } + } catch (ClassNotFoundException e) { + AdtPlugin.log(e, "Problem loading class %1$s or %2$s", //$NON-NLS-1$ + rootClassName, groupClassName); + } catch (InvalidAttributeValueException e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } catch (ClassFormatError e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } catch (IOException e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } + } + + /** + * Parses a View class and adds a ExtViewClassInfo for it in mViewMap. + * It calls itself recursively to handle super classes which are also Views. + */ + private ExtViewClassInfo addView(IClassDescriptor viewClass) { + String fqcn = viewClass.getFullClassName(); + if (mViewMap.containsKey(fqcn)) { + return mViewMap.get(fqcn); + } else if (mGroupMap.containsKey(fqcn)) { + return mGroupMap.get(fqcn); + } + + ExtViewClassInfo info = new ExtViewClassInfo(viewClass.isInstantiable(), + false /* layout */, fqcn, viewClass.getSimpleName()); + mViewMap.put(fqcn, info); + + // All view classes derive from mTopViewClass by design. + // Do not lookup the super class for mTopViewClass itself. + if (viewClass.equals(mTopViewClass) == false) { + IClassDescriptor superClass = viewClass.getSuperclass(); + ExtViewClassInfo superClassInfo = addView(superClass); + info.setSuperClass(superClassInfo); + } + + mAttrsXmlParser.loadViewAttributes(info); + return info; + } + + /** + * Parses a ViewGroup class and adds a ExtViewClassInfo for it in mGroupMap. + * It calls itself recursively to handle super classes which are also ViewGroups. + */ + private ExtViewClassInfo addGroup(IClassDescriptor groupClass) { + String fqcn = groupClass.getFullClassName(); + if (mGroupMap.containsKey(fqcn)) { + return mGroupMap.get(fqcn); + } + + ExtViewClassInfo info = new ExtViewClassInfo(groupClass.isInstantiable(), + true /* layout */, fqcn, groupClass.getSimpleName()); + mGroupMap.put(fqcn, info); + + // All groups derive from android.view.ViewGroup, which in turns derives from + // android.view.View (i.e. mTopViewClass here). So the only group that can have View as + // its super class is the ViewGroup base class and we don't try to resolve it since groups + // are loaded before views. + IClassDescriptor superClass = groupClass.getSuperclass(); + + // Assertion: at this point, we should have + // superClass != mTopViewClass || fqcn.equals(SdkConstants.CLASS_VIEWGROUP); + + if (superClass != null && superClass.equals(mTopViewClass) == false) { + ExtViewClassInfo superClassInfo = addGroup(superClass); + + // Assertion: we should have superClassInfo != null && superClassInfo != info; + if (superClassInfo != null && superClassInfo != info) { + info.setSuperClass(superClassInfo); + } + } + + mAttrsXmlParser.loadViewAttributes(info); + if (mTopLayoutParamsClass != null) { + info.setLayoutParams(addLayoutParams(groupClass)); + } + return info; + } + + /** + * Parses a ViewGroup class and returns an info object on its inner LayoutParams. + * + * @return The {@link LayoutParamsInfo} for the ViewGroup class or null. + */ + private LayoutParamsInfo addLayoutParams(IClassDescriptor groupClass) { + + // Is there a LayoutParams in this group class? + IClassDescriptor layoutParamsClass = findLayoutParams(groupClass); + + // if there's no layout data in the group class, link to the one from the + // super class. + if (layoutParamsClass == null) { + for (IClassDescriptor superClass = groupClass.getSuperclass(); + layoutParamsClass == null && + superClass != null && + superClass.equals(mTopViewClass) == false; + superClass = superClass.getSuperclass()) { + layoutParamsClass = findLayoutParams(superClass); + } + } + + if (layoutParamsClass != null) { + return getLayoutParamsInfo(layoutParamsClass); + } + + return null; + } + + /** + * Parses a LayoutParams class and returns a LayoutParamsInfo object for it. + * It calls itself recursively to handle the super class of the LayoutParams. + */ + private LayoutParamsInfo getLayoutParamsInfo(IClassDescriptor layoutParamsClass) { + String fqcn = layoutParamsClass.getFullClassName(); + LayoutParamsInfo layoutParamsInfo = mLayoutParamsMap.get(fqcn); + + if (layoutParamsInfo != null) { + return layoutParamsInfo; + } + + // Find the link on the LayoutParams super class + LayoutParamsInfo superClassInfo = null; + if (layoutParamsClass.equals(mTopLayoutParamsClass) == false) { + IClassDescriptor superClass = layoutParamsClass.getSuperclass(); + superClassInfo = getLayoutParamsInfo(superClass); + } + + // Find the link on the enclosing ViewGroup + ExtViewClassInfo enclosingGroupInfo = addGroup(layoutParamsClass.getEnclosingClass()); + + layoutParamsInfo = new ExtViewClassInfo.LayoutParamsInfo( + enclosingGroupInfo, layoutParamsClass.getSimpleName(), superClassInfo); + mLayoutParamsMap.put(fqcn, layoutParamsInfo); + + mAttrsXmlParser.loadLayoutParamsAttributes(layoutParamsInfo); + + return layoutParamsInfo; + } + + /** + * Given a ViewGroup-derived class, looks for an inner class named LayoutParams + * and if found returns its class definition. + * <p/> + * This uses the actual defined inner classes and does not look at inherited classes. + * + * @param groupClass The ViewGroup derived class + * @return The Class of the inner LayoutParams or null if none is declared. + */ + private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) { + IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses(); + for (IClassDescriptor innerClass : innerClasses) { + if (innerClass.getSimpleName().equals(SdkConstants.CLASS_NAME_LAYOUTPARAMS)) { + return innerClass; + } + } + return null; + } + + /** + * Computes and return a list of ViewClassInfo from a map by filtering out the class that + * cannot be instantiated. + */ + private List<ViewClassInfo> getInstantiables(SortedMap<String, ExtViewClassInfo> map) { + Collection<ExtViewClassInfo> values = map.values(); + ArrayList<ViewClassInfo> list = new ArrayList<ViewClassInfo>(); + + for (ExtViewClassInfo info : values) { + if (info.isInstantiable()) { + list.add(info); + } + } + + return list; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java new file mode 100644 index 000000000..74c985784 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java @@ -0,0 +1,740 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; + +/** + * Centralized state for Android Eclipse project. + * <p>This gives raw access to the properties (from <code>project.properties</code>), as well + * as direct access to target and library information. + * + * This also gives access to library information. + * + * {@link #isLibrary()} indicates if the project is a library. + * {@link #hasLibraries()} and {@link #getLibraries()} give access to the libraries through + * instances of {@link LibraryState}. A {@link LibraryState} instance is a link between a main + * project and its library. Theses instances are owned by the {@link ProjectState}. + * + * {@link #isMissingLibraries()} will indicate if the project has libraries that are not resolved. + * Unresolved libraries are libraries that do not have any matching opened Eclipse project. + * When there are missing libraries, the {@link LibraryState} instance for them will return null + * for {@link LibraryState#getProjectState()}. + * + */ +public final class ProjectState { + + /** + * A class that represents a library linked to a project. + * <p/>It does not represent the library uniquely. Instead the {@link LibraryState} is linked + * to the main project which is accessible through {@link #getMainProjectState()}. + * <p/>If a library is used by two different projects, then there will be two different + * instances of {@link LibraryState} for the library. + * + * @see ProjectState#getLibrary(IProject) + */ + public final class LibraryState { + private String mRelativePath; + private ProjectState mProjectState; + private String mPath; + + private LibraryState(String relativePath) { + mRelativePath = relativePath; + } + + /** + * Returns the {@link ProjectState} of the main project using this library. + */ + public ProjectState getMainProjectState() { + return ProjectState.this; + } + + /** + * Closes the library. This resets the IProject from this object ({@link #getProjectState()} will + * return <code>null</code>), and updates the main project data so that the library + * {@link IProject} object does not show up in the return value of + * {@link ProjectState#getFullLibraryProjects()}. + */ + public void close() { + mProjectState.removeParentProject(getMainProjectState()); + mProjectState = null; + mPath = null; + + getMainProjectState().updateFullLibraryList(); + } + + private void setRelativePath(String relativePath) { + mRelativePath = relativePath; + } + + private void setProject(ProjectState project) { + mProjectState = project; + mPath = project.getProject().getLocation().toOSString(); + mProjectState.addParentProject(getMainProjectState()); + + getMainProjectState().updateFullLibraryList(); + } + + /** + * Returns the relative path of the library from the main project. + * <p/>This is identical to the value defined in the main project's project.properties. + */ + public String getRelativePath() { + return mRelativePath; + } + + /** + * Returns the {@link ProjectState} item for the library. This can be null if the project + * is not actually opened in Eclipse. + */ + public ProjectState getProjectState() { + return mProjectState; + } + + /** + * Returns the OS-String location of the library project. + * <p/>This is based on location of the Eclipse project that matched + * {@link #getRelativePath()}. + * + * @return The project location, or null if the project is not opened in Eclipse. + */ + public String getProjectLocation() { + return mPath; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LibraryState) { + // the only thing that's always non-null is the relative path. + LibraryState objState = (LibraryState)obj; + return mRelativePath.equals(objState.mRelativePath) && + getMainProjectState().equals(objState.getMainProjectState()); + } else if (obj instanceof ProjectState || obj instanceof IProject) { + return mProjectState != null && mProjectState.equals(obj); + } else if (obj instanceof String) { + return normalizePath(mRelativePath).equals(normalizePath((String) obj)); + } + + return false; + } + + @Override + public int hashCode() { + return normalizePath(mRelativePath).hashCode(); + } + } + + private final IProject mProject; + private final ProjectProperties mProperties; + private IAndroidTarget mTarget; + private BuildToolInfo mBuildToolInfo; + + /** + * list of libraries. Access to this list must be protected by + * <code>synchronized(mLibraries)</code>, but it is important that such code do not call + * out to other classes (especially those protected by {@link Sdk#getLock()}.) + */ + private final ArrayList<LibraryState> mLibraries = new ArrayList<LibraryState>(); + /** Cached list of all IProject instances representing the resolved libraries, including + * indirect dependencies. This must never be null. */ + private List<IProject> mLibraryProjects = Collections.emptyList(); + /** + * List of parent projects. When this instance is a library ({@link #isLibrary()} returns + * <code>true</code>) then this is filled with projects that depends on this project. + */ + private final ArrayList<ProjectState> mParentProjects = new ArrayList<ProjectState>(); + + ProjectState(IProject project, ProjectProperties properties) { + if (project == null || properties == null) { + throw new NullPointerException(); + } + + mProject = project; + mProperties = properties; + + // load the libraries + synchronized (mLibraries) { + int index = 1; + while (true) { + String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++); + String rootPath = mProperties.getProperty(propName); + + if (rootPath == null) { + break; + } + + mLibraries.add(new LibraryState(convertPath(rootPath))); + } + } + } + + public IProject getProject() { + return mProject; + } + + public ProjectProperties getProperties() { + return mProperties; + } + + public @Nullable String getProperty(@NonNull String name) { + if (mProperties != null) { + return mProperties.getProperty(name); + } + + return null; + } + + public void setTarget(IAndroidTarget target) { + mTarget = target; + } + + /** + * Returns the project's target's hash string. + * <p/>If {@link #getTarget()} returns a valid object, then this returns the value of + * {@link IAndroidTarget#hashString()}. + * <p/>Otherwise this will return the value of the property + * {@link ProjectProperties#PROPERTY_TARGET} from {@link #getProperties()} (if valid). + * @return the target hash string or null if not found. + */ + public String getTargetHashString() { + if (mTarget != null) { + return mTarget.hashString(); + } + + return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET); + } + + public IAndroidTarget getTarget() { + return mTarget; + } + + public void setBuildToolInfo(BuildToolInfo buildToolInfo) { + mBuildToolInfo = buildToolInfo; + } + + public BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } + + /** + * Returns the build tools version from the project's properties. + * @return the value or null + */ + @Nullable + public String getBuildToolInfoVersion() { + return mProperties.getProperty(ProjectProperties.PROPERTY_BUILD_TOOLS); + } + + public boolean getRenderScriptSupportMode() { + String supportModeValue = mProperties.getProperty(ProjectProperties.PROPERTY_RS_SUPPORT); + if (supportModeValue != null) { + return Boolean.parseBoolean(supportModeValue); + } + + return false; + } + + public static class LibraryDifference { + public boolean removed = false; + public boolean added = false; + + public boolean hasDiff() { + return removed || added; + } + } + + /** + * Reloads the content of the properties. + * <p/>This also reset the reference to the target as it may have changed, therefore this + * should be followed by a call to {@link Sdk#loadTarget(ProjectState)}. + * + * <p/>If the project libraries changes, they are updated to a certain extent.<br> + * Removed libraries are removed from the state list, and added to the {@link LibraryDifference} + * object that is returned so that they can be processed.<br> + * Added libraries are added to the state (as new {@link LibraryState} objects), but their + * IProject is not resolved. {@link ProjectState#needs(ProjectState)} should be called + * afterwards to properly initialize the libraries. + * + * @return an instance of {@link LibraryDifference} describing the change in libraries. + */ + public LibraryDifference reloadProperties() { + mTarget = null; + mProperties.reload(); + + // compare/reload the libraries. + + // if the order change it won't impact the java part, so instead try to detect removed/added + // libraries. + + LibraryDifference diff = new LibraryDifference(); + + synchronized (mLibraries) { + List<LibraryState> oldLibraries = new ArrayList<LibraryState>(mLibraries); + mLibraries.clear(); + + // load the libraries + int index = 1; + while (true) { + String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++); + String rootPath = mProperties.getProperty(propName); + + if (rootPath == null) { + break; + } + + // search for a library with the same path (not exact same string, but going + // to the same folder). + String convertedPath = convertPath(rootPath); + boolean found = false; + for (int i = 0 ; i < oldLibraries.size(); i++) { + LibraryState libState = oldLibraries.get(i); + if (libState.equals(convertedPath)) { + // it's a match. move it back to mLibraries and remove it from the + // old library list. + found = true; + mLibraries.add(libState); + oldLibraries.remove(i); + break; + } + } + + if (found == false) { + diff.added = true; + mLibraries.add(new LibraryState(convertedPath)); + } + } + + // whatever's left in oldLibraries is removed. + diff.removed = oldLibraries.size() > 0; + + // update the library with what IProjet are known at the time. + updateFullLibraryList(); + } + + return diff; + } + + /** + * Returns the list of {@link LibraryState}. + */ + public List<LibraryState> getLibraries() { + synchronized (mLibraries) { + return Collections.unmodifiableList(mLibraries); + } + } + + /** + * Returns all the <strong>resolved</strong> library projects, including indirect dependencies. + * The list is ordered to match the library priority order for resource processing with + * <code>aapt</code>. + * <p/>If some dependencies are not resolved (or their projects is not opened in Eclipse), + * they will not show up in this list. + * @return the resolved projects as an unmodifiable list. May be an empty. + */ + public List<IProject> getFullLibraryProjects() { + return mLibraryProjects; + } + + /** + * Returns whether this is a library project. + */ + public boolean isLibrary() { + String value = mProperties.getProperty(ProjectProperties.PROPERTY_LIBRARY); + return value != null && Boolean.valueOf(value); + } + + /** + * Returns whether the project depends on one or more libraries. + */ + public boolean hasLibraries() { + synchronized (mLibraries) { + return mLibraries.size() > 0; + } + } + + /** + * Returns whether the project is missing some required libraries. + */ + public boolean isMissingLibraries() { + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + if (state.getProjectState() == null) { + return true; + } + } + } + + return false; + } + + /** + * Returns the {@link LibraryState} object for a given {@link IProject}. + * </p>This can only return a non-null object if the link between the main project's + * {@link IProject} and the library's {@link IProject} was done. + * + * @return the matching LibraryState or <code>null</code> + * + * @see #needs(ProjectState) + */ + public LibraryState getLibrary(IProject library) { + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + ProjectState ps = state.getProjectState(); + if (ps != null && ps.getProject().equals(library)) { + return state; + } + } + } + + return null; + } + + /** + * Returns the {@link LibraryState} object for a given <var>name</var>. + * </p>This can only return a non-null object if the link between the main project's + * {@link IProject} and the library's {@link IProject} was done. + * + * @return the matching LibraryState or <code>null</code> + * + * @see #needs(IProject) + */ + public LibraryState getLibrary(String name) { + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + ProjectState ps = state.getProjectState(); + if (ps != null && ps.getProject().getName().equals(name)) { + return state; + } + } + } + + return null; + } + + + /** + * Returns whether a given library project is needed by the receiver. + * <p/>If the library is needed, this finds the matching {@link LibraryState}, initializes it + * so that it contains the library's {@link IProject} object (so that + * {@link LibraryState#getProjectState()} does not return null) and then returns it. + * + * @param libraryProject the library project to check. + * @return a non null object if the project is a library dependency, + * <code>null</code> otherwise. + * + * @see LibraryState#getProjectState() + */ + public LibraryState needs(ProjectState libraryProject) { + // compute current location + File projectFile = mProject.getLocation().toFile(); + + // get the location of the library. + File libraryFile = libraryProject.getProject().getLocation().toFile(); + + // loop on all libraries and check if the path match + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + if (state.getProjectState() == null) { + File library = new File(projectFile, state.getRelativePath()); + try { + File absPath = library.getCanonicalFile(); + if (absPath.equals(libraryFile)) { + state.setProject(libraryProject); + return state; + } + } catch (IOException e) { + // ignore this library + } + } + } + } + + return null; + } + + /** + * Returns whether the project depends on a given <var>library</var> + * @param library the library to check. + * @return true if the project depends on the library. This is not affected by whether the link + * was done through {@link #needs(ProjectState)}. + */ + public boolean dependsOn(ProjectState library) { + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + if (state != null && state.getProjectState() != null && + library.getProject().equals(state.getProjectState().getProject())) { + return true; + } + } + } + + return false; + } + + + /** + * Updates a library with a new path. + * <p/>This method acts both as a check and an action. If the project does not depend on the + * given <var>oldRelativePath</var> then no action is done and <code>null</code> is returned. + * <p/>If the project depends on the library, then the project is updated with the new path, + * and the {@link LibraryState} for the library is returned. + * <p/>Updating the project does two things:<ul> + * <li>Update LibraryState with new relative path and new {@link IProject} object.</li> + * <li>Update the main project's <code>project.properties</code> with the new relative path + * for the changed library.</li> + * </ul> + * + * @param oldRelativePath the old library path relative to this project + * @param newRelativePath the new library path relative to this project + * @param newLibraryState the new {@link ProjectState} object. + * @return a non null object if the project depends on the library. + * + * @see LibraryState#getProjectState() + */ + public LibraryState updateLibrary(String oldRelativePath, String newRelativePath, + ProjectState newLibraryState) { + // compute current location + File projectFile = mProject.getLocation().toFile(); + + // loop on all libraries and check if the path matches + synchronized (mLibraries) { + for (LibraryState state : mLibraries) { + if (state.getProjectState() == null) { + try { + // oldRelativePath may not be the same exact string as the + // one in the project properties (trailing separator could be different + // for instance). + // Use java.io.File to deal with this and also do a platform-dependent + // path comparison + File library1 = new File(projectFile, oldRelativePath); + File library2 = new File(projectFile, state.getRelativePath()); + if (library1.getCanonicalPath().equals(library2.getCanonicalPath())) { + // save the exact property string to replace. + String oldProperty = state.getRelativePath(); + + // then update the LibraryPath. + state.setRelativePath(newRelativePath); + state.setProject(newLibraryState); + + // update the project.properties file + IStatus status = replaceLibraryProperty(oldProperty, newRelativePath); + if (status != null) { + if (status.getSeverity() != IStatus.OK) { + // log the error somehow. + } + } else { + // This should not happen since the library wouldn't be here in the + // first place + } + + // return the LibraryState object. + return state; + } + } catch (IOException e) { + // ignore this library + } + } + } + } + + return null; + } + + + private void addParentProject(ProjectState parentState) { + mParentProjects.add(parentState); + } + + private void removeParentProject(ProjectState parentState) { + mParentProjects.remove(parentState); + } + + public List<ProjectState> getParentProjects() { + return Collections.unmodifiableList(mParentProjects); + } + + /** + * Computes the transitive closure of projects referencing this project as a + * library project + * + * @return a collection (in any order) of project states for projects that + * directly or indirectly include this project state's project as a + * library project + */ + public Collection<ProjectState> getFullParentProjects() { + Set<ProjectState> result = new HashSet<ProjectState>(); + addParentProjects(result, this); + return result; + } + + /** Adds all parent projects of the given project, transitively, into the given parent set */ + private static void addParentProjects(Set<ProjectState> parents, ProjectState state) { + for (ProjectState s : state.mParentProjects) { + if (!parents.contains(s)) { + parents.add(s); + addParentProjects(parents, s); + } + } + } + + /** + * Update the value of a library dependency. + * <p/>This loops on all current dependency looking for the value to replace and then replaces + * it. + * <p/>This both updates the in-memory {@link #mProperties} values and on-disk + * project.properties file. + * @param oldValue the old value to replace + * @param newValue the new value to set. + * @return the status of the replacement. If null, no replacement was done (value not found). + */ + private IStatus replaceLibraryProperty(String oldValue, String newValue) { + int index = 1; + while (true) { + String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++); + String rootPath = mProperties.getProperty(propName); + + if (rootPath == null) { + break; + } + + if (rootPath.equals(oldValue)) { + // need to update the properties. Get a working copy to change it and save it on + // disk since ProjectProperties is read-only. + ProjectPropertiesWorkingCopy workingCopy = mProperties.makeWorkingCopy(); + workingCopy.setProperty(propName, newValue); + try { + workingCopy.save(); + + // reload the properties with the new values from the disk. + mProperties.reload(); + } catch (Exception e) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( + "Failed to save %1$s for project %2$s", + mProperties.getType() .getFilename(), mProject.getName()), + e); + + } + return Status.OK_STATUS; + } + } + + return null; + } + + /** + * Update the full library list, including indirect dependencies. The result is returned by + * {@link #getFullLibraryProjects()}. + */ + void updateFullLibraryList() { + ArrayList<IProject> list = new ArrayList<IProject>(); + synchronized (mLibraries) { + buildFullLibraryDependencies(mLibraries, list); + } + + mLibraryProjects = Collections.unmodifiableList(list); + } + + /** + * Resolves a given list of libraries, finds out if they depend on other libraries, and + * returns a full list of all the direct and indirect dependencies in the proper order (first + * is higher priority when calling aapt). + * @param inLibraries the libraries to resolve + * @param outLibraries where to store all the libraries. + */ + private void buildFullLibraryDependencies(List<LibraryState> inLibraries, + ArrayList<IProject> outLibraries) { + // loop in the inverse order to resolve dependencies on the libraries, so that if a library + // is required by two higher level libraries it can be inserted in the correct place + for (int i = inLibraries.size() - 1 ; i >= 0 ; i--) { + LibraryState library = inLibraries.get(i); + + // get its libraries if possible + ProjectState libProjectState = library.getProjectState(); + if (libProjectState != null) { + List<LibraryState> dependencies = libProjectState.getLibraries(); + + // build the dependencies for those libraries + buildFullLibraryDependencies(dependencies, outLibraries); + + // and add the current library (if needed) in front (higher priority) + if (outLibraries.contains(libProjectState.getProject()) == false) { + outLibraries.add(0, libProjectState.getProject()); + } + } + } + } + + + /** + * Converts a path containing only / by the proper platform separator. + */ + private String convertPath(String path) { + return path.replaceAll("/", Matcher.quoteReplacement(File.separator)); //$NON-NLS-1$ + } + + /** + * Normalizes a relative path. + */ + private String normalizePath(String path) { + path = convertPath(path); + if (path.endsWith("/")) { //$NON-NLS-1$ + path = path.substring(0, path.length() - 1); + } + return path; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ProjectState) { + return mProject.equals(((ProjectState) obj).mProject); + } else if (obj instanceof IProject) { + return mProject.equals(obj); + } + + return false; + } + + @Override + public int hashCode() { + return mProject.hashCode(); + } + + @Override + public String toString() { + return mProject.getName(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java new file mode 100644 index 000000000..7ff06fc40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java @@ -0,0 +1,1620 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_JAR; +import static com.android.SdkConstants.FD_RES; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.IDevice; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.DexWrapper; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; +import com.android.io.StreamException; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; +import com.android.sdklib.repository.FullRevision; +import com.android.utils.ILogger; +import com.google.common.collect.Maps; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used + * at the same time. + * + * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of + * the Sdk object. + * + * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. + */ +public final class Sdk { + private final static boolean DEBUG = false; + + private final static Object LOCK = new Object(); + + private static Sdk sCurrentSdk = null; + + /** + * Map associating {@link IProject} and their state {@link ProjectState}. + * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}. + */ + private final static HashMap<IProject, ProjectState> sProjectStateMap = + new HashMap<IProject, ProjectState>(); + + /** + * Data bundled using during the load of Target data. + * <p/>This contains the {@link LoadStatus} and a list of projects that attempted + * to compile before the loading was finished. Those projects will be recompiled + * at the end of the loading. + */ + private final static class TargetLoadBundle { + LoadStatus status; + final HashSet<IJavaProject> projectsToReload = new HashSet<IJavaProject>(); + } + + private final SdkManager mManager; + private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap(); + private final AvdManager mAvdManager; + private final DeviceManager mDeviceManager; + + /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */ + private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = + new HashMap<IAndroidTarget, AndroidTargetData>(); + /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */ + private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap = + new HashMap<IAndroidTarget, TargetLoadBundle>(); + + /** + * If true the target data will never load anymore. The only way to reload them is to + * completely reload the SDK with {@link #loadSdk(String)} + */ + private boolean mDontLoadTargetData = false; + + private final String mDocBaseUrl; + + /** + * Classes implementing this interface will receive notification when targets are changed. + */ + public interface ITargetChangeListener { + /** + * Sent when project has its target changed. + */ + void onProjectTargetChange(IProject changedProject); + + /** + * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, + * or the SDK is changed). + */ + void onTargetLoaded(IAndroidTarget target); + + /** + * Called when the base content of the SDK is parsed. + */ + void onSdkLoaded(); + } + + /** + * Basic abstract implementation of the ITargetChangeListener for the case where both + * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)} + * use the same code based on a simple test requiring to know the current IProject. + */ + public static abstract class TargetChangeListener implements ITargetChangeListener { + /** + * Returns the {@link IProject} associated with the listener. + */ + public abstract IProject getProject(); + + /** + * Called when the listener needs to take action on the event. This is only called + * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project + * match the values received in {@link #onProjectTargetChange(IProject)} and + * {@link #onTargetLoaded(IAndroidTarget)}. + */ + public abstract void reload(); + + @Override + public void onProjectTargetChange(IProject changedProject) { + if (changedProject != null && changedProject.equals(getProject())) { + reload(); + } + } + + @Override + public void onTargetLoaded(IAndroidTarget target) { + IProject project = getProject(); + if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) { + reload(); + } + } + + @Override + public void onSdkLoaded() { + // do nothing; + } + } + + /** + * Returns the lock object used to synchronize all operations dealing with SDK, targets and + * projects. + */ + @NonNull + public static final Object getLock() { + return LOCK; + } + + /** + * Loads an SDK and returns an {@link Sdk} object if success. + * <p/>If the SDK failed to load, it displays an error to the user. + * @param sdkLocation the OS path to the SDK. + */ + @Nullable + public static Sdk loadSdk(String sdkLocation) { + synchronized (LOCK) { + if (sCurrentSdk != null) { + sCurrentSdk.dispose(); + sCurrentSdk = null; + } + + final AtomicBoolean hasWarning = new AtomicBoolean(); + final AtomicBoolean hasError = new AtomicBoolean(); + final ArrayList<String> logMessages = new ArrayList<String>(); + ILogger log = new ILogger() { + @Override + public void error(@Nullable Throwable throwable, @Nullable String errorFormat, + Object... arg) { + hasError.set(true); + if (errorFormat != null) { + logMessages.add(String.format("Error: " + errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + hasWarning.set(true); + logMessages.add(String.format("Warning: " + warningFormat, arg)); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + logMessages.add(String.format(msgFormat, arg)); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + info(msgFormat, arg); + } + }; + + // get an SdkManager object for the location + SdkManager manager = SdkManager.createManager(sdkLocation, log); + try { + if (manager == null) { + hasError.set(true); + } else { + // create the AVD Manager + AvdManager avdManager = null; + try { + avdManager = AvdManager.getInstance(manager.getLocalSdk(), log); + } catch (AndroidLocationException e) { + log.error(e, "Error parsing the AVDs"); + } + sCurrentSdk = new Sdk(manager, avdManager); + return sCurrentSdk; + } + } finally { + if (hasError.get() || hasWarning.get()) { + StringBuilder sb = new StringBuilder( + String.format("%s when loading the SDK:\n", + hasError.get() ? "Error" : "Warning")); + for (String msg : logMessages) { + sb.append('\n'); + sb.append(msg); + } + if (hasError.get()) { + AdtPlugin.printErrorToConsole("Android SDK", sb.toString()); + AdtPlugin.displayError("Android SDK", sb.toString()); + } else { + AdtPlugin.printToConsole("Android SDK", sb.toString()); + } + } + } + return null; + } + } + + /** + * Returns the current {@link Sdk} object. + */ + @Nullable + public static Sdk getCurrent() { + synchronized (LOCK) { + return sCurrentSdk; + } + } + + /** + * Returns the location of the current SDK as an OS path string. + * Guaranteed to be terminated by a platform-specific path separator. + * <p/> + * Due to {@link File} canonicalization, this MAY differ from the string used to initialize + * the SDK path. + * + * @return The SDK OS path or null if no SDK is setup. + * @deprecated Consider using {@link #getSdkFileLocation()} instead. + * @see #getSdkFileLocation() + */ + @Deprecated + @Nullable + public String getSdkOsLocation() { + String path = mManager == null ? null : mManager.getLocation(); + if (path != null) { + // For backward compatibility make sure it ends with a separator. + // This used to be the case when the SDK Manager was created from a String path + // but now that a File is internally used the trailing dir separator is lost. + if (path.length() > 0 && !path.endsWith(File.separator)) { + path = path + File.separator; + } + } + return path; + } + + /** + * Returns the location of the current SDK as a {@link File} or null. + * + * @return The SDK OS path or null if no SDK is setup. + */ + @Nullable + public File getSdkFileLocation() { + if (mManager == null || mManager.getLocalSdk() == null) { + return null; + } + return mManager.getLocalSdk().getLocation(); + } + + /** + * Returns a <em>new</em> {@link SdkManager} that can parse the SDK located + * at the current {@link #getSdkOsLocation()}. + * <p/> + * Implementation detail: The {@link Sdk} has its own internal manager with + * a custom logger which is not designed to be useful for outsiders. Callers + * who need their own {@link SdkManager} for parsing will often want to control + * the logger for their own need. + * <p/> + * This is just a convenient method equivalent to writing: + * <pre>SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);</pre> + * + * @param log The logger for the {@link SdkManager}. + * @return A new {@link SdkManager} parsing the same location. + */ + public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) { + return SdkManager.createManager(getSdkOsLocation(), log); + } + + /** + * Returns the URL to the local documentation. + * Can return null if no documentation is found in the current SDK. + * + * @return A file:// URL on the local documentation folder if it exists or null. + */ + @Nullable + public String getDocumentationBaseUrl() { + return mDocBaseUrl; + } + + /** + * Returns the list of targets that are available in the SDK. + */ + public IAndroidTarget[] getTargets() { + return mManager.getTargets(); + } + + /** + * Queries the underlying SDK Manager to check whether the platforms or addons + * directories have changed on-disk. Does not reload the SDK. + * <p/> + * This is a quick test based on the presence of the directories, their timestamps + * and a quick checksum of the source.properties files. It's possible to have + * false positives (e.g. if a file is manually modified in a platform) or false + * negatives (e.g. if a platform data file is changed manually in a 2nd level + * directory without altering the source.properties.) + */ + public boolean haveTargetsChanged() { + return mManager.hasChanged(); + } + + /** + * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. + * + * @param hash the {@link IAndroidTarget} hash string. + * @return The matching {@link IAndroidTarget} or null. + */ + @Nullable + public IAndroidTarget getTargetFromHashString(@NonNull String hash) { + return mManager.getTargetFromHashString(hash); + } + + @Nullable + public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) { + if (buildToolVersion != null) { + try { + return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion)); + } catch (Exception e) { + // ignore, return null below. + } + } + + return null; + } + + @Nullable + public BuildToolInfo getLatestBuildTool() { + return mManager.getLatestBuildTool(); + } + + /** + * Initializes a new project with a target. This creates the <code>project.properties</code> + * file. + * @param project the project to initialize + * @param target the project's target. + * @throws IOException if creating the file failed in any way. + * @throws StreamException if processing the project property file fails + */ + public void initProject(@Nullable IProject project, @Nullable IAndroidTarget target) + throws IOException, StreamException { + if (project == null || target == null) { + return; + } + + synchronized (LOCK) { + // check if there's already a state? + ProjectState state = getProjectState(project); + + ProjectPropertiesWorkingCopy properties = null; + + if (state != null) { + properties = state.getProperties().makeWorkingCopy(); + } + + if (properties == null) { + IPath location = project.getLocation(); + if (location == null) { // can return null when the project is being deleted. + // do nothing and return null; + return; + } + + properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT); + } + + // save the target hash string in the project persistent property + properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); + properties.save(); + } + } + + /** + * Returns the {@link ProjectState} object associated with a given project. + * <p/> + * This method is the only way to properly get the project's {@link ProjectState} + * If the project has not yet been loaded, then it is loaded. + * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk} + * objects, and therefore is static. + * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects + * are replaced. + * @param project the request project + * @return the ProjectState for the project. + */ + @Nullable + @SuppressWarnings("deprecation") + public static ProjectState getProjectState(IProject project) { + if (project == null) { + return null; + } + + synchronized (LOCK) { + ProjectState state = sProjectStateMap.get(project); + if (state == null) { + // load the project.properties from the project folder. + IPath location = project.getLocation(); + if (location == null) { // can return null when the project is being deleted. + // do nothing and return null; + return null; + } + + String projectLocation = location.toOSString(); + + ProjectProperties properties = ProjectProperties.load(projectLocation, + PropertyType.PROJECT); + if (properties == null) { + // legacy support: look for default.properties and rename it if needed. + properties = ProjectProperties.load(projectLocation, + PropertyType.LEGACY_DEFAULT); + + if (properties == null) { + AdtPlugin.log(IStatus.ERROR, + "Failed to load properties file for project '%s'", + project.getName()); + return null; + } else { + //legacy mode. + // get a working copy with the new type "project" + ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy( + PropertyType.PROJECT); + // and save it + try { + wc.save(); + + // delete the old file. + ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT); + + // make sure to use the new properties + properties = ProjectProperties.load(projectLocation, + PropertyType.PROJECT); + } catch (Exception e) { + AdtPlugin.log(IStatus.ERROR, + "Failed to rename properties file to %1$s for project '%s2$'", + PropertyType.PROJECT.getFilename(), project.getName()); + } + } + } + + state = new ProjectState(project, properties); + sProjectStateMap.put(project, state); + + // try to resolve the target + if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) { + sCurrentSdk.loadTargetAndBuildTools(state); + } + } + + return state; + } + } + + /** + * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. + */ + @Nullable + public IAndroidTarget getTarget(IProject project) { + if (project == null) { + return null; + } + + ProjectState state = getProjectState(project); + if (state != null) { + return state.getTarget(); + } + + return null; + } + + /** + * Loads the {@link IAndroidTarget} and BuildTools for a given project. + * <p/>This method will get the target hash string from the project properties, and resolve + * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}. + * @param state the state representing the project to load. + * @return the target that was loaded. + */ + @Nullable + public IAndroidTarget loadTargetAndBuildTools(ProjectState state) { + IAndroidTarget target = null; + if (state != null) { + String hash = state.getTargetHashString(); + if (hash != null) { + state.setTarget(target = getTargetFromHashString(hash)); + } + + String markerMessage = null; + String buildToolInfoVersion = state.getBuildToolInfoVersion(); + if (buildToolInfoVersion != null) { + BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion); + + if (buildToolsInfo != null) { + state.setBuildToolInfo(buildToolsInfo); + } else { + markerMessage = String.format("Unable to resolve %s property value '%s'", + ProjectProperties.PROPERTY_BUILD_TOOLS, + buildToolInfoVersion); + } + } else { + // this is ok, we'll use the latest one automatically. + state.setBuildToolInfo(null); + } + + handleBuildToolsMarker(state.getProject(), markerMessage); + } + + return target; + } + + /** + * Adds or edit a build tools marker from the given project. This is done through a Job. + * @param project the project + * @param markerMessage the message. if null the marker is removed. + */ + private void handleBuildToolsMarker(final IProject project, final String markerMessage) { + Job markerJob = new Job("Android SDK: Build Tools Marker") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (project.isAccessible()) { + // always delete existing marker first + project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true, + IResource.DEPTH_ZERO); + + // add the new one if needed. + if (markerMessage != null) { + BaseProjectHelper.markProject(project, + AdtConstants.MARKER_BUILD_TOOLS, + markerMessage, IMarker.SEVERITY_ERROR, + IMarker.PRIORITY_HIGH); + } + } + } catch (CoreException e2) { + AdtPlugin.log(e2, null); + // Don't return e2.getStatus(); the job control will then produce + // a popup with this error, which isn't very interesting for the + // user. + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); + markerJob.schedule(); + } + + /** + * Checks and loads (if needed) the data for a given target. + * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified + * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. + * <p/>An optional project as second parameter can be given to be recompiled once the target + * data is finished loading. + * <p/>The return value is non-null only if the target data has already been loaded (and in this + * case is the status of the load operation) + * @param target the target to load. + * @param project an optional project to be recompiled when the target data is loaded. + * If the target is already loaded, nothing happens. + * @return The load status if the target data is already loaded. + */ + @NonNull + public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) { + boolean loadData = false; + + synchronized (LOCK) { + if (mDontLoadTargetData) { + return LoadStatus.FAILED; + } + + TargetLoadBundle bundle = mTargetDataStatusMap.get(target); + if (bundle == null) { + bundle = new TargetLoadBundle(); + mTargetDataStatusMap.put(target,bundle); + + // set status to loading + bundle.status = LoadStatus.LOADING; + + // add project to bundle + if (project != null) { + bundle.projectsToReload.add(project); + } + + // and set the flag to start the loading below + loadData = true; + } else if (bundle.status == LoadStatus.LOADING) { + // add project to bundle + if (project != null) { + bundle.projectsToReload.add(project); + } + + return bundle.status; + } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) { + return bundle.status; + } + } + + if (loadData) { + Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) { + @Override + protected IStatus run(IProgressMonitor monitor) { + AdtPlugin plugin = AdtPlugin.getDefault(); + try { + IStatus status = new AndroidTargetParser(target).run(monitor); + + IJavaProject[] javaProjectArray = null; + + synchronized (LOCK) { + TargetLoadBundle bundle = mTargetDataStatusMap.get(target); + + if (status.getCode() != IStatus.OK) { + bundle.status = LoadStatus.FAILED; + bundle.projectsToReload.clear(); + } else { + bundle.status = LoadStatus.LOADED; + + // Prepare the array of project to recompile. + // The call is done outside of the synchronized block. + javaProjectArray = bundle.projectsToReload.toArray( + new IJavaProject[bundle.projectsToReload.size()]); + + // and update the UI of the editors that depend on the target data. + plugin.updateTargetListeners(target); + } + } + + if (javaProjectArray != null) { + ProjectHelper.updateProjects(javaProjectArray); + } + + return status; + } catch (Throwable t) { + synchronized (LOCK) { + TargetLoadBundle bundle = mTargetDataStatusMap.get(target); + bundle.status = LoadStatus.FAILED; + } + + AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$ + String message = String.format("Parsing Data for %1$s failed", target.hashString()); + if (t instanceof UnsupportedClassVersionError) { + message = "To use this platform, run Eclipse with JDK 7 or later. (" + message + ")"; + } + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, t); + } + } + }; + job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs + job.setRule(ResourcesPlugin.getWorkspace().getRoot()); + job.schedule(); + } + + // The only way to go through here is when the loading starts through the Job. + // Therefore the current status of the target is LOADING. + return LoadStatus.LOADING; + } + + /** + * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. + */ + @Nullable + public AndroidTargetData getTargetData(IAndroidTarget target) { + synchronized (LOCK) { + return mTargetDataMap.get(target); + } + } + + /** + * Return the {@link AndroidTargetData} for a given {@link IProject}. + */ + @Nullable + public AndroidTargetData getTargetData(IProject project) { + synchronized (LOCK) { + IAndroidTarget target = getTarget(project); + if (target != null) { + return getTargetData(target); + } + } + + return null; + } + + /** + * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not + * loaded properly, then this will return <code>null</code>. + */ + @Nullable + public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) { + if (buildToolInfo == null) { + return null; + } + synchronized (LOCK) { + String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR); + DexWrapper dexWrapper = mDexWrappers.get(dexLocation); + + if (dexWrapper == null) { + // load DX. + dexWrapper = new DexWrapper(); + IStatus res = dexWrapper.loadDex(dexLocation); + if (res != Status.OK_STATUS) { + AdtPlugin.log(null, res.getMessage()); + dexWrapper = null; + } else { + mDexWrappers.put(dexLocation, dexWrapper); + } + } + + return dexWrapper; + } + } + + public void unloadDexWrappers() { + synchronized (LOCK) { + for (DexWrapper wrapper : mDexWrappers.values()) { + wrapper.unload(); + } + mDexWrappers.clear(); + } + } + + /** + * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could + * be <code>null</code>. + */ + @Nullable + public AvdManager getAvdManager() { + return mAvdManager; + } + + @Nullable + public static AndroidVersion getDeviceVersion(@NonNull IDevice device) { + try { + Map<String, String> props = device.getProperties(); + String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL); + if (apiLevel == null) { + return null; + } + + return new AndroidVersion(Integer.parseInt(apiLevel), + props.get((IDevice.PROP_BUILD_CODENAME))); + } catch (NumberFormatException e) { + return null; + } + } + + @NonNull + public DeviceManager getDeviceManager() { + return mDeviceManager; + } + + /** + * Returns a list of {@link ProjectState} representing projects depending, directly or + * indirectly on a given library project. + * @param project the library project. + * @return a possibly empty list of ProjectState. + */ + @NonNull + public static Set<ProjectState> getMainProjectsFor(IProject project) { + synchronized (LOCK) { + // first get the project directly depending on this. + Set<ProjectState> list = new HashSet<ProjectState>(); + + // loop on all project and see if ProjectState.getLibrary returns a non null + // project. + for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) { + if (project != entry.getKey()) { + LibraryState library = entry.getValue().getLibrary(project); + if (library != null) { + list.add(entry.getValue()); + } + } + } + + // now look for projects depending on the projects directly depending on the library. + HashSet<ProjectState> result = new HashSet<ProjectState>(list); + for (ProjectState p : list) { + if (p.isLibrary()) { + Set<ProjectState> set = getMainProjectsFor(p.getProject()); + result.addAll(set); + } + } + + return result; + } + } + + /** + * Unload the SDK's target data. + * + * If <var>preventReload</var>, this effect is final until the SDK instance is changed + * through {@link #loadSdk(String)}. + * + * The goal is to unload the targets to be able to replace existing targets with new ones, + * before calling {@link #loadSdk(String)} to fully reload the SDK. + * + * @param preventReload prevent the data from being loaded again for the remaining live of + * this {@link Sdk} instance. + */ + public void unloadTargetData(boolean preventReload) { + synchronized (LOCK) { + mDontLoadTargetData = preventReload; + + // dispose of the target data. + for (AndroidTargetData data : mTargetDataMap.values()) { + data.dispose(); + } + + mTargetDataMap.clear(); + } + } + + private Sdk(SdkManager manager, AvdManager avdManager) { + mManager = manager; + mAvdManager = avdManager; + + // listen to projects closing + GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); + // need to register the resource event listener first because the project listener + // is called back during registration with project opened in the workspace. + monitor.addResourceEventListener(mResourceEventListener); + monitor.addProjectListener(mProjectListener); + monitor.addFileListener(mFileListener, + IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED); + + // pre-compute some paths + mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() + + SdkConstants.OS_SDK_DOCS_FOLDER); + + mDeviceManager = DeviceManager.createInstance(manager.getLocalSdk().getLocation(), + AdtPlugin.getDefault()); + + // update whatever ProjectState is already present with new IAndroidTarget objects. + synchronized (LOCK) { + for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { + loadTargetAndBuildTools(entry.getValue()); + } + } + } + + /** + * Cleans and unloads the SDK. + */ + private void dispose() { + GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor(); + monitor.removeProjectListener(mProjectListener); + monitor.removeFileListener(mFileListener); + monitor.removeResourceEventListener(mResourceEventListener); + + // the IAndroidTarget objects are now obsolete so update the project states. + synchronized (LOCK) { + for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { + entry.getValue().setTarget(null); + } + + // dispose of the target data. + for (AndroidTargetData data : mTargetDataMap.values()) { + data.dispose(); + } + + mTargetDataMap.clear(); + } + } + + void setTargetData(IAndroidTarget target, AndroidTargetData data) { + synchronized (LOCK) { + mTargetDataMap.put(target, data); + } + } + + /** + * Returns the URL to the local documentation. + * Can return null if no documentation is found in the current SDK. + * + * @param osDocsPath Path to the documentation folder in the current SDK. + * The folder may not actually exist. + * @return A file:// URL on the local documentation folder if it exists or null. + */ + private String getDocumentationBaseUrl(String osDocsPath) { + File f = new File(osDocsPath); + + if (f.isDirectory()) { + try { + // Note: to create a file:// URL, one would typically use something like + // f.toURI().toURL().toString(). However this generates a broken path on + // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of + // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll + // do the correct thing manually. + + String path = f.getAbsolutePath(); + if (File.separatorChar != '/') { + path = path.replace(File.separatorChar, '/'); + } + + // For some reason the URL class doesn't add the mandatory "//" after + // the "file:" protocol name, so it has to be hacked into the path. + URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ + String result = url.toString(); + return result; + } catch (MalformedURLException e) { + // ignore malformed URLs + } + } + + return null; + } + + /** + * Delegate listener for project changes. + */ + private IProjectListener mProjectListener = new IProjectListener() { + @Override + public void projectClosed(IProject project) { + onProjectRemoved(project, false /*deleted*/); + } + + @Override + public void projectDeleted(IProject project) { + onProjectRemoved(project, true /*deleted*/); + } + + private void onProjectRemoved(IProject removedProject, boolean deleted) { + if (DEBUG) { + System.out.println(">>> CLOSED: " + removedProject.getName()); + } + + // get the target project + synchronized (LOCK) { + // Don't use getProject() as it could create the ProjectState if it's not + // there yet and this is not what we want. We want the current object. + // Therefore, direct access to the map. + ProjectState removedState = sProjectStateMap.get(removedProject); + if (removedState != null) { + // 1. clear the layout lib cache associated with this project + IAndroidTarget target = removedState.getTarget(); + if (target != null) { + // get the bridge for the target, and clear the cache for this project. + AndroidTargetData data = mTargetDataMap.get(target); + if (data != null) { + LayoutLibrary layoutLib = data.getLayoutLibrary(); + if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) { + layoutLib.clearCaches(removedProject); + } + } + } + + // 2. if the project is a library, make sure to update the + // LibraryState for any project referencing it. + // Also, record the updated projects that are libraries, to update + // projects that depend on them. + for (ProjectState projectState : sProjectStateMap.values()) { + LibraryState libState = projectState.getLibrary(removedProject); + if (libState != null) { + // Close the library right away. + // This remove links between the LibraryState and the projectState. + // This is because in case of a rename of a project, projectClosed and + // projectOpened will be called before any other job is run, so we + // need to make sure projectOpened is closed with the main project + // state up to date. + libState.close(); + + // record that this project changed, and in case it's a library + // that its parents need to be updated as well. + markProject(projectState, projectState.isLibrary()); + } + } + + // now remove the project for the project map. + sProjectStateMap.remove(removedProject); + } + } + + if (DEBUG) { + System.out.println("<<<"); + } + } + + @Override + public void projectOpened(IProject project) { + onProjectOpened(project); + } + + @Override + public void projectOpenedWithWorkspace(IProject project) { + // no need to force recompilation when projects are opened with the workspace. + onProjectOpened(project); + } + + @Override + public void allProjectsOpenedWithWorkspace() { + // Correct currently open editors + fixOpenLegacyEditors(); + } + + private void onProjectOpened(final IProject openedProject) { + + ProjectState openedState = getProjectState(openedProject); + if (openedState != null) { + if (DEBUG) { + System.out.println(">>> OPENED: " + openedProject.getName()); + } + + synchronized (LOCK) { + final boolean isLibrary = openedState.isLibrary(); + final boolean hasLibraries = openedState.hasLibraries(); + + if (isLibrary || hasLibraries) { + boolean foundLibraries = false; + // loop on all the existing project and update them based on this new + // project + for (ProjectState projectState : sProjectStateMap.values()) { + if (projectState != openedState) { + // If the project has libraries, check if this project + // is a reference. + if (hasLibraries) { + // ProjectState#needs() both checks if this is a missing library + // and updates LibraryState to contains the new values. + // This must always be called. + LibraryState libState = openedState.needs(projectState); + + if (libState != null) { + // found a library! Add the main project to the list of + // modified project + foundLibraries = true; + } + } + + // if the project is a library check if the other project depend + // on it. + if (isLibrary) { + // ProjectState#needs() both checks if this is a missing library + // and updates LibraryState to contains the new values. + // This must always be called. + LibraryState libState = projectState.needs(openedState); + + if (libState != null) { + // There's a dependency! Add the project to the list of + // modified project, but also to a list of projects + // that saw one of its dependencies resolved. + markProject(projectState, projectState.isLibrary()); + } + } + } + } + + // if the project has a libraries and we found at least one, we add + // the project to the list of modified project. + // Since we already went through the parent, no need to update them. + if (foundLibraries) { + markProject(openedState, false /*updateParents*/); + } + } + } + + // Correct file editor associations. + fixEditorAssociations(openedProject); + + // Fix classpath entries in a job since the workspace might be locked now. + Job fixCpeJob = new Job("Adjusting Android Project Classpath") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + ProjectHelper.fixProjectClasspathEntries( + JavaCore.create(openedProject)); + } catch (JavaModelException e) { + AdtPlugin.log(e, "error fixing classpath entries"); + // Don't return e2.getStatus(); the job control will then produce + // a popup with this error, which isn't very interesting for the + // user. + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + fixCpeJob.setPriority(Job.BUILD); + fixCpeJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); + fixCpeJob.schedule(); + + + if (DEBUG) { + System.out.println("<<<"); + } + } + } + + @Override + public void projectRenamed(IProject project, IPath from) { + // we don't actually care about this anymore. + } + }; + + /** + * Delegate listener for file changes. + */ + private IFileListener mFileListener = new IFileListener() { + @Override + public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidPRoject) { + if (!isAndroidPRoject) { + return; + } + + if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) && + file.getParent() == file.getProject()) { + try { + // reload the content of the project.properties file and update + // the target. + IProject iProject = file.getProject(); + + ProjectState state = Sdk.getProjectState(iProject); + + // get the current target and build tools + IAndroidTarget oldTarget = state.getTarget(); + boolean oldRsSupportMode = state.getRenderScriptSupportMode(); + + // get the current library flag + boolean wasLibrary = state.isLibrary(); + + LibraryDifference diff = state.reloadProperties(); + + // load the (possibly new) target. + IAndroidTarget newTarget = loadTargetAndBuildTools(state); + + // reload the libraries if needed + if (diff.hasDiff()) { + if (diff.added) { + synchronized (LOCK) { + for (ProjectState projectState : sProjectStateMap.values()) { + if (projectState != state) { + // need to call needs to do the libraryState link, + // but no need to look at the result, as we'll compare + // the result of getFullLibraryProjects() + // this is easier to due to indirect dependencies. + state.needs(projectState); + } + } + } + } + + markProject(state, wasLibrary || state.isLibrary()); + } + + // apply the new target if needed. + if (newTarget != oldTarget || + oldRsSupportMode != state.getRenderScriptSupportMode()) { + IJavaProject javaProject = BaseProjectHelper.getJavaProject( + file.getProject()); + if (javaProject != null) { + ProjectHelper.updateProject(javaProject); + } + + // update the editors to reload with the new target + AdtPlugin.getDefault().updateTargetListeners(iProject); + } + } catch (CoreException e) { + // This can't happen as it's only for closed project (or non existing) + // but in that case we can't get a fileChanged on this file. + } + } else if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) { + // check if it's an add/remove on a jar files inside libs + if (EXT_JAR.equals(extension) && + file.getProjectRelativePath().segmentCount() == 2 && + file.getParent().getName().equals(SdkConstants.FD_NATIVE_LIBS)) { + // need to update the project and whatever depend on it. + + processJarFileChange(file); + } + } + } + + private void processJarFileChange(final IFile file) { + try { + IProject iProject = file.getProject(); + + if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + return; + } + + List<IJavaProject> projectList = new ArrayList<IJavaProject>(); + IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject); + if (javaProject != null) { + projectList.add(javaProject); + } + + ProjectState state = Sdk.getProjectState(iProject); + + if (state != null) { + Collection<ProjectState> parents = state.getFullParentProjects(); + for (ProjectState s : parents) { + javaProject = BaseProjectHelper.getJavaProject(s.getProject()); + if (javaProject != null) { + projectList.add(javaProject); + } + } + + ProjectHelper.updateProjects( + projectList.toArray(new IJavaProject[projectList.size()])); + } + } catch (CoreException e) { + // This can't happen as it's only for closed project (or non existing) + // but in that case we can't get a fileChanged on this file. + } + } + }; + + /** List of modified projects. This is filled in + * {@link IProjectListener#projectOpened(IProject)}, + * {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, + * {@link IProjectListener#projectClosed(IProject)}, and + * {@link IProjectListener#projectDeleted(IProject)} and processed in + * {@link IResourceEventListener#resourceChangeEventEnd()}. + */ + private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>(); + private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>(); + + private void markProject(ProjectState projectState, boolean updateParents) { + if (mModifiedProjects.contains(projectState) == false) { + if (DEBUG) { + System.out.println("\tMARKED: " + projectState.getProject().getName()); + } + mModifiedProjects.add(projectState); + } + + // if the project is resolved also add it to this list. + if (updateParents) { + if (mModifiedChildProjects.contains(projectState) == false) { + if (DEBUG) { + System.out.println("\tMARKED(child): " + projectState.getProject().getName()); + } + mModifiedChildProjects.add(projectState); + } + } + } + + /** + * Delegate listener for resource changes. This is called before and after any calls to the + * project and file listeners (for a given resource change event). + */ + private IResourceEventListener mResourceEventListener = new IResourceEventListener() { + @Override + public void resourceChangeEventStart() { + mModifiedProjects.clear(); + mModifiedChildProjects.clear(); + } + + @Override + public void resourceChangeEventEnd() { + if (mModifiedProjects.size() == 0) { + return; + } + + // first make sure all the parents are updated + updateParentProjects(); + + // for all modified projects, update their library list + // and gather their IProject + final List<IJavaProject> projectList = new ArrayList<IJavaProject>(); + for (ProjectState state : mModifiedProjects) { + state.updateFullLibraryList(); + projectList.add(JavaCore.create(state.getProject())); + } + + Job job = new Job("Android Library Update") { //$NON-NLS-1$ + @Override + protected IStatus run(IProgressMonitor monitor) { + LibraryClasspathContainerInitializer.updateProjects( + projectList.toArray(new IJavaProject[projectList.size()])); + + for (IJavaProject javaProject : projectList) { + try { + javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, + monitor); + } catch (CoreException e) { + // pass + } + } + return Status.OK_STATUS; + } + }; + job.setPriority(Job.BUILD); + job.setRule(ResourcesPlugin.getWorkspace().getRoot()); + job.schedule(); + } + }; + + /** + * Updates all existing projects with a given list of new/updated libraries. + * This loops through all opened projects and check if they depend on any of the given + * library project, and if they do, they are linked together. + */ + private void updateParentProjects() { + if (mModifiedChildProjects.size() == 0) { + return; + } + + ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects); + mModifiedChildProjects.clear(); + synchronized (LOCK) { + // for each project for which we must update its parent, we loop on the parent + // projects and adds them to the list of modified projects. If they are themselves + // libraries, we add them too. + for (ProjectState state : childProjects) { + if (DEBUG) { + System.out.println(">>> Updating parents of " + state.getProject().getName()); + } + List<ProjectState> parents = state.getParentProjects(); + for (ProjectState parent : parents) { + markProject(parent, parent.isLibrary()); + } + if (DEBUG) { + System.out.println("<<<"); + } + } + } + + // done, but there may be parents that are also libraries. Need to update their parents. + updateParentProjects(); + } + + /** + * Fix editor associations for the given project, if not already done. + * <p/> + * Eclipse has a per-file setting for which editor should be used for each file + * (see {@link IDE#setDefaultEditor(IFile, String)}). + * We're using this flag to pick between the various XML editors (layout, drawable, etc) + * since they all have the same file name extension. + * <p/> + * Unfortunately, the file setting can be "wrong" for two reasons: + * <ol> + * <li> The editor type was added <b>after</b> a file had been seen by the IDE. + * For example, we added new editors for animations and for drawables around + * ADT 12, but any file seen by ADT in earlier versions will continue to use + * the vanilla Eclipse XML editor instead. + * <li> A bug in ADT 14 and ADT 15 (see issue 21124) meant that files created in new + * folders would end up with wrong editor associations. Even though that bug + * is fixed in ADT 16, the fix only affects new files, it cannot retroactively + * fix editor associations that were set incorrectly by ADT 14 or 15. + * </ol> + * <p/> + * This method attempts to fix the editor bindings retroactively by scanning all the + * resource XML files and resetting the editor associations. + * Since this is a potentially slow operation, this is only done "once"; we use a + * persistent project property to avoid looking repeatedly. In the future if we add + * additional editors, we can rev the scanned version value. + */ + private void fixEditorAssociations(final IProject project) { + QualifiedName KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "editorbinding"); //$NON-NLS-1$ + + try { + String value = project.getPersistentProperty(KEY); + int currentVersion = 0; + if (value != null) { + try { + currentVersion = Integer.parseInt(value); + } catch (Exception ingore) { + } + } + + // The target version we're comparing to. This must be incremented each time + // we change the processing here so that a new version of the plugin would + // try to fix existing user projects. + final int targetVersion = 2; + + if (currentVersion >= targetVersion) { + return; + } + + // Set to specific version such that we can rev the version in the future + // to trigger further scanning + project.setPersistentProperty(KEY, Integer.toString(targetVersion)); + + // Now update the actual editor associations. + Job job = new Job("Update Android editor bindings") { //$NON-NLS-1$ + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + for (IResource folderResource : project.getFolder(FD_RES).members()) { + if (folderResource instanceof IFolder) { + IFolder folder = (IFolder) folderResource; + + for (IResource resource : folder.members()) { + if (resource instanceof IFile && + resource.getName().endsWith(DOT_XML)) { + fixXmlFile((IFile) resource); + } + } + } + } + + // TODO change AndroidManifest.xml ID too + + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return Status.OK_STATUS; + } + + /** + * Attempt to fix the editor ID for the given /res XML file. + */ + private void fixXmlFile(final IFile file) { + // Fix the default editor ID for this resource. + // This has no effect on currently open editors. + IEditorDescriptor desc = IDE.getDefaultEditor(file); + + if (desc == null || !CommonXmlEditor.ID.equals(desc.getId())) { + IDE.setDefaultEditor(file, CommonXmlEditor.ID); + } + } + }; + job.setPriority(Job.BUILD); + job.schedule(); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + /** + * Tries to fix all currently open Android legacy editors. + * <p/> + * If an editor is found to match one of the legacy ids, we'll try to close it. + * If that succeeds, we try to reopen it using the new common editor ID. + * <p/> + * This method must be run from the UI thread. + */ + private void fixOpenLegacyEditors() { + + AdtPlugin adt = AdtPlugin.getDefault(); + if (adt == null) { + return; + } + + final IPreferenceStore store = adt.getPreferenceStore(); + int currentValue = store.getInt(AdtPrefs.PREFS_FIX_LEGACY_EDITORS); + // The target version we're comparing to. This must be incremented each time + // we change the processing here so that a new version of the plugin would + // try to fix existing editors. + final int targetValue = 1; + + if (currentValue >= targetValue) { + return; + } + + // To be able to close and open editors we need to make sure this is done + // in the UI thread, which this isn't invoked from. + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + HashSet<String> legacyIds = + new HashSet<String>(Arrays.asList(CommonXmlEditor.LEGACY_EDITOR_IDS)); + + for (IWorkbenchWindow win : PlatformUI.getWorkbench().getWorkbenchWindows()) { + for (IWorkbenchPage page : win.getPages()) { + for (IEditorReference ref : page.getEditorReferences()) { + try { + IEditorInput input = ref.getEditorInput(); + if (input instanceof IFileEditorInput) { + IFile file = ((IFileEditorInput)input).getFile(); + IEditorPart part = ref.getEditor(true /*restore*/); + if (part != null) { + IWorkbenchPartSite site = part.getSite(); + if (site != null) { + String id = site.getId(); + if (legacyIds.contains(id)) { + // This editor matches one of legacy editor IDs. + fixEditor(page, part, input, file, id); + } + } + } + } + } catch (Exception e) { + // ignore + } + } + } + } + + // Remember that we managed to do fix all editors + store.setValue(AdtPrefs.PREFS_FIX_LEGACY_EDITORS, targetValue); + } + + private void fixEditor( + IWorkbenchPage page, + IEditorPart part, + IEditorInput input, + IFile file, + String id) { + IDE.setDefaultEditor(file, CommonXmlEditor.ID); + + boolean ok = page.closeEditor(part, true /*save*/); + + AdtPlugin.log(IStatus.INFO, + "Closed legacy editor ID %s for %s: %s", //$NON-NLS-1$ + id, + file.getFullPath(), + ok ? "Success" : "Failed");//$NON-NLS-1$ //$NON-NLS-2$ + + if (ok) { + // Try to reopen it with the new ID + try { + page.openEditor(input, CommonXmlEditor.ID); + } catch (PartInitException e) { + AdtPlugin.log(e, + "Failed to reopen %s", //$NON-NLS-1$ + file.getFullPath()); + } + } + } + }); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/WidgetClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/WidgetClassLoader.java new file mode 100644 index 000000000..682d6e538 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/WidgetClassLoader.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.sdk; + +import com.android.SdkConstants; + +import org.eclipse.core.runtime.IProgressMonitor; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import javax.management.InvalidAttributeValueException; + +/** + * Parser for the text file containing the list of widgets, layouts and layout params. + * <p/> + * The file is a straight text file containing one class per line.<br> + * Each line is in the following format<br> + * <code>[code][class name] [super class name] [super class name]...</code> + * where code is a single letter (W for widget, L for layout, P for layout params), and class names + * are the fully qualified name of the classes. + */ +public final class WidgetClassLoader implements IAndroidClassLoader { + + /** + * Basic class containing the class descriptions found in the text file. + */ + private final static class ClassDescriptor implements IClassDescriptor { + + private String mFqcn; + private String mSimpleName; + private ClassDescriptor mSuperClass; + private ClassDescriptor mEnclosingClass; + private final ArrayList<IClassDescriptor> mDeclaredClasses = + new ArrayList<IClassDescriptor>(); + private boolean mIsInstantiable = false; + + ClassDescriptor(String fqcn) { + mFqcn = fqcn; + mSimpleName = getSimpleName(fqcn); + } + + @Override + public String getFullClassName() { + return mFqcn; + } + + @Override + public String getSimpleName() { + return mSimpleName; + } + + @Override + public IClassDescriptor[] getDeclaredClasses() { + return mDeclaredClasses.toArray(new IClassDescriptor[mDeclaredClasses.size()]); + } + + private void addDeclaredClass(ClassDescriptor declaredClass) { + mDeclaredClasses.add(declaredClass); + } + + @Override + public IClassDescriptor getEnclosingClass() { + return mEnclosingClass; + } + + void setEnclosingClass(ClassDescriptor enclosingClass) { + // set the enclosing class. + mEnclosingClass = enclosingClass; + + // add this to the list of declared class in the enclosing class. + mEnclosingClass.addDeclaredClass(this); + + // finally change the name of declared class to make sure it uses the + // convention: package.enclosing$declared instead of package.enclosing.declared + mFqcn = enclosingClass.mFqcn + "$" + mFqcn.substring(enclosingClass.mFqcn.length() + 1); + } + + @Override + public IClassDescriptor getSuperclass() { + return mSuperClass; + } + + void setSuperClass(ClassDescriptor superClass) { + mSuperClass = superClass; + } + + @Override + public boolean equals(Object clazz) { + if (clazz instanceof ClassDescriptor) { + return mFqcn.equals(((ClassDescriptor)clazz).mFqcn); + } + return super.equals(clazz); + } + + @Override + public int hashCode() { + return mFqcn.hashCode(); + } + + @Override + public boolean isInstantiable() { + return mIsInstantiable; + } + + void setInstantiable(boolean state) { + mIsInstantiable = state; + } + + private String getSimpleName(String fqcn) { + String[] segments = fqcn.split("\\."); + return segments[segments.length-1]; + } + } + + private BufferedReader mReader; + + /** Output map of FQCN => descriptor on all classes */ + private final Map<String, ClassDescriptor> mMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on View classes */ + private final Map<String, ClassDescriptor> mWidgetMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on ViewGroup classes */ + private final Map<String, ClassDescriptor> mLayoutMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on LayoutParams classes */ + private final Map<String, ClassDescriptor> mLayoutParamsMap = + new HashMap<String, ClassDescriptor>(); + /** File path of the source text file */ + private String mOsFilePath; + + /** + * Creates a loader with a given file path. + * @param osFilePath the OS path of the file to load. + * @throws FileNotFoundException if the file is not found. + */ + WidgetClassLoader(String osFilePath) throws FileNotFoundException { + mOsFilePath = osFilePath; + mReader = new BufferedReader(new FileReader(osFilePath)); + } + + @Override + public String getSource() { + return mOsFilePath; + } + + /** + * Parses the text file and return true if the file was successfully parsed. + * @param monitor + */ + boolean parseWidgetList(IProgressMonitor monitor) { + try { + String line; + while ((line = mReader.readLine()) != null) { + if (line.length() > 0) { + char prefix = line.charAt(0); + String[] classes = null; + ClassDescriptor clazz = null; + switch (prefix) { + case 'W': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, null /* map */); + if (clazz != null) { + clazz.setInstantiable(true); + mWidgetMap.put(classes[0], clazz); + } + break; + case 'L': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, null /* map */); + if (clazz != null) { + clazz.setInstantiable(true); + mLayoutMap.put(classes[0], clazz); + } + break; + case 'P': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, mLayoutParamsMap); + if (clazz != null) { + clazz.setInstantiable(true); + } + break; + case '#': + // comment, do nothing + break; + default: + throw new IllegalArgumentException(); + } + } + } + + // reconciliate the layout and their layout params + postProcess(); + + return true; + } catch (IOException e) { + } finally { + try { + mReader.close(); + } catch (IOException e) { + } + } + + return false; + } + + /** + * Parses a View class and adds a ViewClassInfo for it in mWidgetMap. + * It calls itself recursively to handle super classes which are also Views. + * @param classes the inheritance list of the class to process. + * @param index the index of the class to process in the <code>classes</code> array. + * @param map an optional map in which to put every {@link ClassDescriptor} created. + */ + private ClassDescriptor processClass(String[] classes, int index, + Map<String, ClassDescriptor> map) { + if (index >= classes.length) { + return null; + } + + String fqcn = classes[index]; + + if ("java.lang.Object".equals(fqcn)) { //$NON-NLS-1$ + return null; + } + + // check if the ViewInfoClass has not yet been created. + if (mMap.containsKey(fqcn)) { + return mMap.get(fqcn); + } + + // create the custom class. + ClassDescriptor clazz = new ClassDescriptor(fqcn); + mMap.put(fqcn, clazz); + if (map != null) { + map.put(fqcn, clazz); + } + + // get the super class + ClassDescriptor superClass = processClass(classes, index+1, map); + if (superClass != null) { + clazz.setSuperClass(superClass); + } + + return clazz; + } + + /** + * Goes through the layout params and look for the enclosed class. If the layout params + * has no known enclosed type it is dropped. + */ + private void postProcess() { + Collection<ClassDescriptor> params = mLayoutParamsMap.values(); + + for (ClassDescriptor param : params) { + String fqcn = param.getFullClassName(); + + // get the enclosed name. + String enclosed = getEnclosedName(fqcn); + + // look for a match in the layouts. We don't use the layout map as it only contains the + // end classes, but in this case we also need to process the layout params for the base + // layout classes. + ClassDescriptor enclosingType = mMap.get(enclosed); + if (enclosingType != null) { + param.setEnclosingClass(enclosingType); + + // remove the class from the map, and put it back with the fixed name + mMap.remove(fqcn); + mMap.put(param.getFullClassName(), param); + } + } + } + + private String getEnclosedName(String fqcn) { + int index = fqcn.lastIndexOf('.'); + return fqcn.substring(0, index); + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * + * @param rootPackage Root package of classes to find. Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + @Override + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(String rootPackage, + String[] superClasses) throws IOException, InvalidAttributeValueException, + ClassFormatError { + HashMap<String, ArrayList<IClassDescriptor>> map = + new HashMap<String, ArrayList<IClassDescriptor>>(); + + ArrayList<IClassDescriptor> list = new ArrayList<IClassDescriptor>(); + list.addAll(mWidgetMap.values()); + map.put(SdkConstants.CLASS_VIEW, list); + + list = new ArrayList<IClassDescriptor>(); + list.addAll(mLayoutMap.values()); + map.put(SdkConstants.CLASS_VIEWGROUP, list); + + list = new ArrayList<IClassDescriptor>(); + list.addAll(mLayoutParamsMap.values()); + map.put(SdkConstants.CLASS_VIEWGROUP_LAYOUTPARAMS, list); + + return map; + } + + /** + * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + @Override + public IClassDescriptor getClass(String className) throws ClassNotFoundException { + return mMap.get(className); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/layout-devices.xsd b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/layout-devices.xsd new file mode 100755 index 000000000..a4ea6c42e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/layout-devices.xsd @@ -0,0 +1,345 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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. +--> +<xsd:schema + targetNamespace="http://schemas.android.com/sdk/android/layout-devices/1" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:c="http://schemas.android.com/sdk/android/layout-devices/1" + elementFormDefault="qualified" + attributeFormDefault="unqualified" + version="1"> + + <!-- The root element layout-devices defines a sequence of 0..n device elements. --> + + <xsd:element name="layout-devices" type="c:layoutDevicesType" /> + + <xsd:complexType name="layoutDevicesType"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + The "layout-devices" element is the root element of this schema. + + It must contain zero or more "device" elements that each define the configurations + available for a given device. + + These definitions are used in the Graphical Layout Editor in the + Android Development Tools (ADT) plugin for Eclipse. + </xsd:documentation> + </xsd:annotation> + + <xsd:sequence> + <!-- layout-devices defines a sequence of 0..n device elements. --> + <xsd:element name="device" minOccurs="0" maxOccurs="unbounded"> + + <xsd:annotation> + <xsd:documentation xml:lang="en"> + A device element must contain at most one "default" element + followed by one or more "config" elements. + + The "default" element defines all the default parameters + inherited by the following "config" elements. + Each "config" element can override the default values, if any. + + A "device" element also has a required "name" attribute that + represents the user-interface name of this device. + </xsd:documentation> + </xsd:annotation> + + <xsd:complexType> + <!-- device defines a choice of 0..1 default element + and 1..n config elements. --> + + <xsd:sequence> + <xsd:element name="default" type="c:parametersType" + minOccurs="0" maxOccurs="1" /> + <xsd:element name="config" type="c:configType" + minOccurs="1" maxOccurs="unbounded" /> + </xsd:sequence> + + <xsd:attribute name="name" type="xsd:normalizedString" use="required" /> + </xsd:complexType> + + </xsd:element> + </xsd:sequence> + </xsd:complexType> + + <!-- The type of a device>default element. + This is overridden by configType below for the device>config element. + --> + <xsd:complexType name="parametersType"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + The parametersType define all the parameters that can happen either in a + "default" element or in a named "config" element. + Each parameter element can appear once at most. + + Parameters here are the same as those used to specify alternate Android + resources, as documented by + http://d.android.com/guide/topics/resources/resources-i18n.html#AlternateResources + </xsd:documentation> + </xsd:annotation> + + <xsd:all> + <!-- parametersType says that 0..1 of each of these elements must be declared. --> + + <xsd:element name="country-code" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the configuration is for a particular Mobile Country Code. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:float"> + <xsd:minInclusive value="100" /> + <xsd:maxInclusive value="999" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="network-code" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the configuration is for a particular Mobile Network Code. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:float"> + <xsd:minExclusive value="0" /> + <xsd:maxExclusive value="1000" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="screen-size" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies that the configuration is for a particular class of screen. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="small" /> + <xsd:enumeration value="normal" /> + <xsd:enumeration value="large" /> + <xsd:enumeration value="xlarge" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="screen-ratio" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies that the configuration is for a taller/wider than traditional + screen. This is based purely on the aspect ration of the screen: QVGA, + HVGA, and VGA are notlong; WQVGA, WVGA, FWVGA are long. Note that long + may mean either wide or tall, depending on the current orientation. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="long" /> + <xsd:enumeration value="notlong" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="screen-orientation" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies that the configuration is for a screen that is tall (port) or + wide (land). + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="port" /> + <xsd:enumeration value="land" /> + <xsd:enumeration value="square" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="pixel-density" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the screen density the configuration is defined for. The medium + density of traditional HVGA screens (mdpi) is defined to be approximately + 160dpi; low density (ldpi) is 120, and high density (hdpi) is 240. There + is thus a 4:3 scaling factor between each density, so a 9x9 bitmap in ldpi + would be 12x12 is mdpi and 16x16 in hdpi. + The special nodpi density that can be used in resource qualifiers is not + a valid keyword here. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="ldpi" /> + <xsd:enumeration value="mdpi" /> + <xsd:enumeration value="tvdpi" /> + <xsd:enumeration value="hdpi" /> + <xsd:enumeration value="xhdpi" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="touch-type" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the touch type the configuration is defined for. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="notouch" /> + <xsd:enumeration value="stylus" /> + <xsd:enumeration value="finger" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="keyboard-state" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + If your configuration uses a soft keyboard, use the keyssoft value. + If it doesn't and has a real keyboard, use keysexposed or keyshidden. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="keysexposed" /> + <xsd:enumeration value="keyshidden" /> + <xsd:enumeration value="keyssoft" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="text-input-method" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the primary text input method the configuration is designed for. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="nokeys" /> + <xsd:enumeration value="qwerty" /> + <xsd:enumeration value="12key" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="nav-state" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies whether the primary non-touchscreen navigation control is + exposed or hidden. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="navexposed" /> + <xsd:enumeration value="navhidden" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="nav-method" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the primary non-touchscreen navigation method the configuration + is designed for. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="dpad" /> + <xsd:enumeration value="trackball" /> + <xsd:enumeration value="wheel" /> + <xsd:enumeration value="nonav" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="screen-dimension" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the device screen resolution, in pixels. + </xsd:documentation> + </xsd:annotation> + <xsd:complexType> + <xsd:sequence minOccurs="2" maxOccurs="2"> + + <xsd:element name="size"> + <xsd:simpleType> + <xsd:restriction base="xsd:positiveInteger" /> + </xsd:simpleType> + </xsd:element> + + </xsd:sequence> + </xsd:complexType> + </xsd:element> + + <xsd:element name="xdpi" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the actual density in X of the device screen. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:float"> + <xsd:minExclusive value="0" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + <xsd:element name="ydpi" minOccurs="0"> + <xsd:annotation> + <xsd:documentation xml:lang="en"> + Specifies the actual density in Y of the device screen. + </xsd:documentation> + </xsd:annotation> + <xsd:simpleType> + <xsd:restriction base="xsd:float"> + <xsd:minExclusive value="0" /> + </xsd:restriction> + </xsd:simpleType> + </xsd:element> + + </xsd:all> + </xsd:complexType> + + <!-- The type definition of a device>config element. + This type is basically all the element defined by parametersType and an extra + required "name" attribute for the user-interface configuration name. + --> + <xsd:complexType name="configType"> + <xsd:annotation> + <xsd:documentation> + The configType defines the content of a "config" element in a "device" element. + + A "config" element can have all the parameters elements defined by + "parameterType". It also has a required "name" attribute that indicates the + user-interface name for this configuration. + </xsd:documentation> + </xsd:annotation> + + <xsd:complexContent> + <xsd:extension base="c:parametersType"> + <xsd:attribute name="name" type="xsd:normalizedString" use="required" /> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + +</xsd:schema> |