1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.project;
18 
19 import static com.android.ide.eclipse.adt.AdtConstants.COMPILER_COMPLIANCE_PREFERRED;
20 
21 import com.android.SdkConstants;
22 import com.android.annotations.NonNull;
23 import com.android.ide.common.xml.ManifestData;
24 import com.android.ide.eclipse.adt.AdtConstants;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.build.builders.PostCompilerBuilder;
27 import com.android.ide.eclipse.adt.internal.build.builders.PreCompilerBuilder;
28 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
29 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
30 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
31 import com.android.sdklib.BuildToolInfo;
32 import com.android.sdklib.IAndroidTarget;
33 import com.android.utils.Pair;
34 
35 import org.eclipse.core.resources.ICommand;
36 import org.eclipse.core.resources.IFile;
37 import org.eclipse.core.resources.IFolder;
38 import org.eclipse.core.resources.IMarker;
39 import org.eclipse.core.resources.IProject;
40 import org.eclipse.core.resources.IProjectDescription;
41 import org.eclipse.core.resources.IResource;
42 import org.eclipse.core.resources.IWorkspace;
43 import org.eclipse.core.resources.IncrementalProjectBuilder;
44 import org.eclipse.core.resources.ResourcesPlugin;
45 import org.eclipse.core.runtime.CoreException;
46 import org.eclipse.core.runtime.IPath;
47 import org.eclipse.core.runtime.IProgressMonitor;
48 import org.eclipse.core.runtime.NullProgressMonitor;
49 import org.eclipse.core.runtime.Path;
50 import org.eclipse.core.runtime.QualifiedName;
51 import org.eclipse.jdt.core.IClasspathEntry;
52 import org.eclipse.jdt.core.IJavaModel;
53 import org.eclipse.jdt.core.IJavaProject;
54 import org.eclipse.jdt.core.JavaCore;
55 import org.eclipse.jdt.core.JavaModelException;
56 import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
57 import org.eclipse.jdt.launching.IVMInstall;
58 import org.eclipse.jdt.launching.IVMInstall2;
59 import org.eclipse.jdt.launching.IVMInstallType;
60 import org.eclipse.jdt.launching.JavaRuntime;
61 
62 import java.util.ArrayList;
63 import java.util.HashMap;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.TreeMap;
67 
68 /**
69  * Utility class to manipulate Project parameters/properties.
70  */
71 public final class ProjectHelper {
72     public final static int COMPILER_COMPLIANCE_OK = 0;
73     public final static int COMPILER_COMPLIANCE_LEVEL = 1;
74     public final static int COMPILER_COMPLIANCE_SOURCE = 2;
75     public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3;
76 
77     /**
78      * Adds the given ClasspathEntry object to the class path entries.
79      * This method does not check whether the entry is already defined in the project.
80      *
81      * @param entries The class path entries to read. A copy will be returned.
82      * @param newEntry The new class path entry to add.
83      * @return A new class path entries array.
84      */
addEntryToClasspath( IClasspathEntry[] entries, IClasspathEntry newEntry)85     public static IClasspathEntry[] addEntryToClasspath(
86             IClasspathEntry[] entries, IClasspathEntry newEntry) {
87         int n = entries.length;
88         IClasspathEntry[] newEntries = new IClasspathEntry[n + 1];
89         System.arraycopy(entries, 0, newEntries, 0, n);
90         newEntries[n] = newEntry;
91         return newEntries;
92     }
93 
94     /**
95      * Replaces the given ClasspathEntry in the classpath entries.
96      *
97      * If the classpath does not yet exists (Check is based on entry path), then it is added.
98      *
99      * @param entries The class path entries to read. The same array (replace) or a copy (add)
100      *                will be returned.
101      * @param newEntry The new class path entry to add.
102      * @return The same array (replace) or a copy (add) will be returned.
103      *
104      * @see IClasspathEntry#getPath()
105      */
replaceEntryInClasspath( IClasspathEntry[] entries, IClasspathEntry newEntry)106     public static IClasspathEntry[] replaceEntryInClasspath(
107             IClasspathEntry[] entries, IClasspathEntry newEntry) {
108 
109         IPath path = newEntry.getPath();
110         for (int i = 0, count = entries.length; i < count ; i++) {
111             if (path.equals(entries[i].getPath())) {
112                 entries[i] = newEntry;
113                 return entries;
114             }
115         }
116 
117         return addEntryToClasspath(entries, newEntry);
118     }
119 
120     /**
121      * Adds the corresponding source folder to the project's class path entries.
122      * This method does not check whether the entry is already defined in the project.
123      *
124      * @param javaProject The java project of which path entries to update.
125      * @param newEntry The new class path entry to add.
126      * @throws JavaModelException
127      */
addEntryToClasspath(IJavaProject javaProject, IClasspathEntry newEntry)128     public static void addEntryToClasspath(IJavaProject javaProject, IClasspathEntry newEntry)
129             throws JavaModelException {
130 
131         IClasspathEntry[] entries = javaProject.getRawClasspath();
132         entries = addEntryToClasspath(entries, newEntry);
133         javaProject.setRawClasspath(entries, new NullProgressMonitor());
134     }
135 
136     /**
137      * Checks whether the given class path entry is already defined in the project.
138      *
139      * @param javaProject The java project of which path entries to check.
140      * @param newEntry The parent source folder to remove.
141      * @return True if the class path entry is already defined.
142      * @throws JavaModelException
143      */
isEntryInClasspath(IJavaProject javaProject, IClasspathEntry newEntry)144     public static boolean isEntryInClasspath(IJavaProject javaProject, IClasspathEntry newEntry)
145             throws JavaModelException {
146 
147         IClasspathEntry[] entries = javaProject.getRawClasspath();
148         for (IClasspathEntry entry : entries) {
149             if (entry.equals(newEntry)) {
150                 return true;
151             }
152         }
153         return false;
154     }
155 
156     /**
157      * Remove a classpath entry from the array.
158      * @param entries The class path entries to read. A copy will be returned
159      * @param index The index to remove.
160      * @return A new class path entries array.
161      */
removeEntryFromClasspath( IClasspathEntry[] entries, int index)162     public static IClasspathEntry[] removeEntryFromClasspath(
163             IClasspathEntry[] entries, int index) {
164         int n = entries.length;
165         IClasspathEntry[] newEntries = new IClasspathEntry[n-1];
166 
167         // copy the entries before index
168         System.arraycopy(entries, 0, newEntries, 0, index);
169 
170         // copy the entries after index
171         System.arraycopy(entries, index + 1, newEntries, index,
172                 entries.length - index - 1);
173 
174         return newEntries;
175     }
176 
177     /**
178      * Converts a OS specific path into a path valid for the java doc location
179      * attributes of a project.
180      * @param javaDocOSLocation The OS specific path.
181      * @return a valid path for the java doc location.
182      */
getJavaDocPath(String javaDocOSLocation)183     public static String getJavaDocPath(String javaDocOSLocation) {
184         // first thing we do is convert the \ into /
185         String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$
186                 AdtConstants.WS_SEP);
187 
188         // then we add file: at the beginning for unix path, and file:/ for non
189         // unix path
190         if (javaDoc.startsWith(AdtConstants.WS_SEP)) {
191             return "file:" + javaDoc; //$NON-NLS-1$
192         }
193 
194         return "file:/" + javaDoc; //$NON-NLS-1$
195     }
196 
197     /**
198      * Look for a specific classpath entry by full path and return its index.
199      * @param entries The entry array to search in.
200      * @param entryPath The OS specific path of the entry.
201      * @param entryKind The kind of the entry. Accepted values are 0
202      * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT,
203      * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE,
204      * and IClasspathEntry.CPE_CONTAINER
205      * @return the index of the found classpath entry or -1.
206      */
findClasspathEntryByPath(IClasspathEntry[] entries, String entryPath, int entryKind)207     public static int findClasspathEntryByPath(IClasspathEntry[] entries,
208             String entryPath, int entryKind) {
209         for (int i = 0 ; i < entries.length ; i++) {
210             IClasspathEntry entry = entries[i];
211 
212             int kind = entry.getEntryKind();
213 
214             if (kind == entryKind || entryKind == 0) {
215                 // get the path
216                 IPath path = entry.getPath();
217 
218                 String osPathString = path.toOSString();
219                 if (osPathString.equals(entryPath)) {
220                     return i;
221                 }
222             }
223         }
224 
225         // not found, return bad index.
226         return -1;
227     }
228 
229     /**
230      * Look for a specific classpath entry for file name only and return its
231      *  index.
232      * @param entries The entry array to search in.
233      * @param entryName The filename of the entry.
234      * @param entryKind The kind of the entry. Accepted values are 0
235      * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT,
236      * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE,
237      * and IClasspathEntry.CPE_CONTAINER
238      * @param startIndex Index where to start the search
239      * @return the index of the found classpath entry or -1.
240      */
findClasspathEntryByName(IClasspathEntry[] entries, String entryName, int entryKind, int startIndex)241     public static int findClasspathEntryByName(IClasspathEntry[] entries,
242             String entryName, int entryKind, int startIndex) {
243         if (startIndex < 0) {
244             startIndex = 0;
245         }
246         for (int i = startIndex ; i < entries.length ; i++) {
247             IClasspathEntry entry = entries[i];
248 
249             int kind = entry.getEntryKind();
250 
251             if (kind == entryKind || entryKind == 0) {
252                 // get the path
253                 IPath path = entry.getPath();
254                 String name = path.segment(path.segmentCount()-1);
255 
256                 if (name.equals(entryName)) {
257                     return i;
258                 }
259             }
260         }
261 
262         // not found, return bad index.
263         return -1;
264     }
265 
updateProject(IJavaProject project)266     public static boolean updateProject(IJavaProject project) {
267         return updateProjects(new IJavaProject[] { project});
268     }
269 
270     /**
271      * Update the android-specific projects's classpath containers.
272      * @param projects the projects to update
273      * @return
274      */
updateProjects(IJavaProject[] projects)275     public static boolean updateProjects(IJavaProject[] projects) {
276         boolean r = AndroidClasspathContainerInitializer.updateProjects(projects);
277         if (r) {
278             return LibraryClasspathContainerInitializer.updateProjects(projects);
279         }
280         return false;
281     }
282 
283     /**
284      * Fix the project. This checks the SDK location.
285      * @param project The project to fix.
286      * @throws JavaModelException
287      */
fixProject(IProject project)288     public static void fixProject(IProject project) throws JavaModelException {
289         if (AdtPlugin.getOsSdkFolder().length() == 0) {
290             AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed.");
291             return;
292         }
293 
294         // get a java project
295         IJavaProject javaProject = JavaCore.create(project);
296         fixProjectClasspathEntries(javaProject);
297     }
298 
299     /**
300      * Fix the project classpath entries. The method ensures that:
301      * <ul>
302      * <li>The project does not reference any old android.zip/android.jar archive.</li>
303      * <li>The project does not use its output folder as a sourc folder.</li>
304      * <li>The project does not reference a desktop JRE</li>
305      * <li>The project references the AndroidClasspathContainer.
306      * </ul>
307      * @param javaProject The project to fix.
308      * @throws JavaModelException
309      */
fixProjectClasspathEntries(IJavaProject javaProject)310     public static void fixProjectClasspathEntries(IJavaProject javaProject)
311             throws JavaModelException {
312 
313         // get the project classpath
314         IClasspathEntry[] entries = javaProject.getRawClasspath();
315         IClasspathEntry[] oldEntries = entries;
316         boolean forceRewriteOfCPE = false;
317 
318         // check if the JRE is set as library
319         int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER,
320                 IClasspathEntry.CPE_CONTAINER);
321         if (jreIndex != -1) {
322             // the project has a JRE included, we remove it
323             entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex);
324         }
325 
326         // get the output folder
327         IPath outputFolder = javaProject.getOutputLocation();
328 
329         boolean foundFrameworkContainer = false;
330         IClasspathEntry foundLibrariesContainer = null;
331         IClasspathEntry foundDependenciesContainer = null;
332 
333         for (int i = 0 ; i < entries.length ;) {
334             // get the entry and kind
335             IClasspathEntry entry = entries[i];
336             int kind = entry.getEntryKind();
337 
338             if (kind == IClasspathEntry.CPE_SOURCE) {
339                 IPath path = entry.getPath();
340 
341                 if (path.equals(outputFolder)) {
342                     entries = ProjectHelper.removeEntryFromClasspath(entries, i);
343 
344                     // continue, to skip the i++;
345                     continue;
346                 }
347             } else if (kind == IClasspathEntry.CPE_CONTAINER) {
348                 String path = entry.getPath().toString();
349                 if (AdtConstants.CONTAINER_FRAMEWORK.equals(path)) {
350                     foundFrameworkContainer = true;
351                 } else if (AdtConstants.CONTAINER_PRIVATE_LIBRARIES.equals(path)) {
352                     foundLibrariesContainer = entry;
353                 } else if (AdtConstants.CONTAINER_DEPENDENCIES.equals(path)) {
354                     foundDependenciesContainer = entry;
355                 }
356             }
357 
358             i++;
359         }
360 
361         // look to see if we have the m2eclipse nature
362         boolean m2eNature = false;
363         try {
364             m2eNature = javaProject.getProject().hasNature("org.eclipse.m2e.core.maven2Nature");
365         } catch (CoreException e) {
366             AdtPlugin.log(e, "Failed to query project %s for m2e nature",
367                     javaProject.getProject().getName());
368         }
369 
370 
371         // if the framework container is not there, we add it
372         if (!foundFrameworkContainer) {
373             // add the android container to the array
374             entries = ProjectHelper.addEntryToClasspath(entries,
375                     JavaCore.newContainerEntry(new Path(AdtConstants.CONTAINER_FRAMEWORK)));
376         }
377 
378         // same thing for the library container
379         if (foundLibrariesContainer == null) {
380             // add the exported libraries android container to the array
381             entries = ProjectHelper.addEntryToClasspath(entries,
382                     JavaCore.newContainerEntry(
383                             new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), true));
384         } else if (!m2eNature && !foundLibrariesContainer.isExported()) {
385             // the container is present but it's not exported and since there's no m2e nature
386             // we do want it to be exported.
387             // keep all the other parameters the same.
388             entries = ProjectHelper.replaceEntryInClasspath(entries,
389                     JavaCore.newContainerEntry(
390                             new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES),
391                             foundLibrariesContainer.getAccessRules(),
392                             foundLibrariesContainer.getExtraAttributes(),
393                             true));
394             forceRewriteOfCPE = true;
395         }
396 
397         // same thing for the dependencies container
398         if (foundDependenciesContainer == null) {
399             // add the android dependencies container to the array
400             entries = ProjectHelper.addEntryToClasspath(entries,
401                     JavaCore.newContainerEntry(
402                             new Path(AdtConstants.CONTAINER_DEPENDENCIES), true));
403         } else if (!m2eNature && !foundDependenciesContainer.isExported()) {
404             // the container is present but it's not exported and since there's no m2e nature
405             // we do want it to be exported.
406             // keep all the other parameters the same.
407             entries = ProjectHelper.replaceEntryInClasspath(entries,
408                     JavaCore.newContainerEntry(
409                             new Path(AdtConstants.CONTAINER_DEPENDENCIES),
410                             foundDependenciesContainer.getAccessRules(),
411                             foundDependenciesContainer.getExtraAttributes(),
412                             true));
413             forceRewriteOfCPE = true;
414         }
415 
416         // set the new list of entries to the project
417         if (entries != oldEntries || forceRewriteOfCPE) {
418             javaProject.setRawClasspath(entries, new NullProgressMonitor());
419         }
420 
421         // If needed, check and fix compiler compliance and source compatibility
422         ProjectHelper.checkAndFixCompilerCompliance(javaProject);
423     }
424 
425 
426     /**
427      * Checks the project compiler compliance level is supported.
428      * @param javaProject The project to check
429      * @return A pair with the first integer being an error code, and the second value
430      *   being the invalid value found or null. The error code can be: <ul>
431      * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li>
432      * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li>
433      * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li>
434      * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li>
435      * </ul>
436      */
checkCompilerCompliance(IJavaProject javaProject)437     public static final Pair<Integer, String> checkCompilerCompliance(IJavaProject javaProject) {
438         // get the project compliance level option
439         String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
440 
441         // check it against a list of valid compliance level strings.
442         if (!checkCompliance(javaProject, compliance)) {
443             // if we didn't find the proper compliance level, we return an error
444             return Pair.of(COMPILER_COMPLIANCE_LEVEL, compliance);
445         }
446 
447         // otherwise we check source compatibility
448         String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
449 
450         // check it against a list of valid compliance level strings.
451         if (!checkCompliance(javaProject, source)) {
452             // if we didn't find the proper compliance level, we return an error
453             return Pair.of(COMPILER_COMPLIANCE_SOURCE, source);
454         }
455 
456         // otherwise check codegen level
457         String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true);
458 
459         // check it against a list of valid compliance level strings.
460         if (!checkCompliance(javaProject, codeGen)) {
461             // if we didn't find the proper compliance level, we return an error
462             return Pair.of(COMPILER_COMPLIANCE_CODEGEN_TARGET, codeGen);
463         }
464 
465         return Pair.of(COMPILER_COMPLIANCE_OK, null);
466     }
467 
468     /**
469      * Checks the project compiler compliance level is supported.
470      * @param project The project to check
471      * @return A pair with the first integer being an error code, and the second value
472      *   being the invalid value found or null. The error code can be: <ul>
473      * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li>
474      * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li>
475      * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li>
476      * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li>
477      * </ul>
478      */
checkCompilerCompliance(IProject project)479     public static final Pair<Integer, String> checkCompilerCompliance(IProject project) {
480         // get the java project from the IProject resource object
481         IJavaProject javaProject = JavaCore.create(project);
482 
483         // check and return the result.
484         return checkCompilerCompliance(javaProject);
485     }
486 
487 
488     /**
489      * Checks, and fixes if needed, the compiler compliance level, and the source compatibility
490      * level
491      * @param project The project to check and fix.
492      */
checkAndFixCompilerCompliance(IProject project)493     public static final void checkAndFixCompilerCompliance(IProject project) {
494         // FIXME This method is never used. Shall we just removed it?
495         // {@link #checkAndFixCompilerCompliance(IJavaProject)} is used instead.
496 
497         // get the java project from the IProject resource object
498         IJavaProject javaProject = JavaCore.create(project);
499 
500         // Now we check the compiler compliance level and make sure it is valid
501         checkAndFixCompilerCompliance(javaProject);
502     }
503 
504     /**
505      * Checks, and fixes if needed, the compiler compliance level, and the source compatibility
506      * level
507      * @param javaProject The Java project to check and fix.
508      */
checkAndFixCompilerCompliance(IJavaProject javaProject)509     public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) {
510         Pair<Integer, String> result = checkCompilerCompliance(javaProject);
511         if (result.getFirst().intValue() != COMPILER_COMPLIANCE_OK) {
512             // setup the preferred compiler compliance level.
513             javaProject.setOption(JavaCore.COMPILER_COMPLIANCE,
514                     AdtConstants.COMPILER_COMPLIANCE_PREFERRED);
515             javaProject.setOption(JavaCore.COMPILER_SOURCE,
516                     AdtConstants.COMPILER_COMPLIANCE_PREFERRED);
517             javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM,
518                     AdtConstants.COMPILER_COMPLIANCE_PREFERRED);
519 
520             // clean the project to make sure we recompile
521             try {
522                 javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD,
523                         new NullProgressMonitor());
524             } catch (CoreException e) {
525                 AdtPlugin.printErrorToConsole(javaProject.getProject(),
526                         "Project compiler settings changed. Clean your project.");
527             }
528         }
529     }
530 
531     /**
532      * Makes the given project use JDK 6 (or more specifically,
533      * {@link AdtConstants#COMPILER_COMPLIANCE_PREFERRED} as the compilation
534      * target, regardless of what the default IDE JDK level is, provided a JRE
535      * of the given level is installed.
536      *
537      * @param javaProject the Java project
538      * @throws CoreException if the IDE throws an exception setting the compiler
539      *             level
540      */
541     @SuppressWarnings("restriction") // JDT API for setting compliance options
enforcePreferredCompilerCompliance(@onNull IJavaProject javaProject)542     public static void enforcePreferredCompilerCompliance(@NonNull IJavaProject javaProject)
543             throws CoreException {
544         String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
545         if (compliance == null ||
546                 JavaModelUtil.isVersionLessThan(compliance, COMPILER_COMPLIANCE_PREFERRED)) {
547             IVMInstallType[] types = JavaRuntime.getVMInstallTypes();
548             for (int i = 0; i < types.length; i++) {
549                 IVMInstallType type = types[i];
550                 IVMInstall[] installs = type.getVMInstalls();
551                 for (int j = 0; j < installs.length; j++) {
552                     IVMInstall install = installs[j];
553                     if (install instanceof IVMInstall2) {
554                         IVMInstall2 install2 = (IVMInstall2) install;
555                         // Java version can be 1.6.0, and preferred is 1.6
556                         if (install2.getJavaVersion().startsWith(COMPILER_COMPLIANCE_PREFERRED)) {
557                             Map<String, String> options = javaProject.getOptions(false);
558                             JavaCore.setComplianceOptions(COMPILER_COMPLIANCE_PREFERRED, options);
559                             JavaModelUtil.setDefaultClassfileOptions(options,
560                                     COMPILER_COMPLIANCE_PREFERRED);
561                             javaProject.setOptions(options);
562                             return;
563                         }
564                     }
565                 }
566             }
567         }
568     }
569 
570     /**
571      * Returns a {@link IProject} by its running application name, as it returned by the AVD.
572      * <p/>
573      * <var>applicationName</var> will in most case be the package declared in the manifest, but
574      * can, in some cases, be a custom process name declared in the manifest, in the
575      * <code>application</code>, <code>activity</code>, <code>receiver</code>, or
576      * <code>service</code> nodes.
577      * @param applicationName The application name.
578      * @return a project or <code>null</code> if no matching project were found.
579      */
findAndroidProjectByAppName(String applicationName)580     public static IProject findAndroidProjectByAppName(String applicationName) {
581         // Get the list of project for the current workspace
582         IWorkspace workspace = ResourcesPlugin.getWorkspace();
583         IProject[] projects = workspace.getRoot().getProjects();
584 
585         // look for a project that matches the packageName of the app
586         // we're trying to debug
587         for (IProject p : projects) {
588             if (p.isOpen()) {
589                 try {
590                     if (p.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
591                         // ignore non android projects
592                         continue;
593                     }
594                 } catch (CoreException e) {
595                     // failed to get the nature? skip project.
596                     continue;
597                 }
598 
599                 // check that there is indeed a manifest file.
600                 IFile manifestFile = getManifest(p);
601                 if (manifestFile == null) {
602                     // no file? skip this project.
603                     continue;
604                 }
605 
606                 ManifestData data = AndroidManifestHelper.parseForData(manifestFile);
607                 if (data == null) {
608                     // skip this project.
609                     continue;
610                 }
611 
612                 String manifestPackage = data.getPackage();
613 
614                 if (manifestPackage != null && manifestPackage.equals(applicationName)) {
615                     // this is the project we were looking for!
616                     return p;
617                 } else {
618                     // if the package and application name don't match,
619                     // we look for other possible process names declared in the manifest.
620                     String[] processes = data.getProcesses();
621                     for (String process : processes) {
622                         if (process.equals(applicationName)) {
623                             return p;
624                         }
625                     }
626                 }
627             }
628         }
629 
630         return null;
631 
632     }
633 
fixProjectNatureOrder(IProject project)634     public static void fixProjectNatureOrder(IProject project) throws CoreException {
635         IProjectDescription description = project.getDescription();
636         String[] natures = description.getNatureIds();
637 
638         // if the android nature is not the first one, we reorder them
639         if (AdtConstants.NATURE_DEFAULT.equals(natures[0]) == false) {
640             // look for the index
641             for (int i = 0 ; i < natures.length ; i++) {
642                 if (AdtConstants.NATURE_DEFAULT.equals(natures[i])) {
643                     // if we try to just reorder the array in one pass, this doesn't do
644                     // anything. I guess JDT check that we are actually adding/removing nature.
645                     // So, first we'll remove the android nature, and then add it back.
646 
647                     // remove the android nature
648                     removeNature(project, AdtConstants.NATURE_DEFAULT);
649 
650                     // now add it back at the first index.
651                     description = project.getDescription();
652                     natures = description.getNatureIds();
653 
654                     String[] newNatures = new String[natures.length + 1];
655 
656                     // first one is android
657                     newNatures[0] = AdtConstants.NATURE_DEFAULT;
658 
659                     // next the rest that was before the android nature
660                     System.arraycopy(natures, 0, newNatures, 1, natures.length);
661 
662                     // set the new natures
663                     description.setNatureIds(newNatures);
664                     project.setDescription(description, null);
665 
666                     // and stop
667                     break;
668                 }
669             }
670         }
671     }
672 
673 
674     /**
675      * Removes a specific nature from a project.
676      * @param project The project to remove the nature from.
677      * @param nature The nature id to remove.
678      * @throws CoreException
679      */
removeNature(IProject project, String nature)680     public static void removeNature(IProject project, String nature) throws CoreException {
681         IProjectDescription description = project.getDescription();
682         String[] natures = description.getNatureIds();
683 
684         // check if the project already has the android nature.
685         for (int i = 0; i < natures.length; ++i) {
686             if (nature.equals(natures[i])) {
687                 String[] newNatures = new String[natures.length - 1];
688                 if (i > 0) {
689                     System.arraycopy(natures, 0, newNatures, 0, i);
690                 }
691                 System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1);
692                 description.setNatureIds(newNatures);
693                 project.setDescription(description, null);
694 
695                 return;
696             }
697         }
698 
699     }
700 
701     /**
702      * Returns if the project has error level markers.
703      * @param includeReferencedProjects flag to also test the referenced projects.
704      * @throws CoreException
705      */
hasError(IProject project, boolean includeReferencedProjects)706     public static boolean hasError(IProject project, boolean includeReferencedProjects)
707     throws CoreException {
708         IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
709         if (markers != null && markers.length > 0) {
710             // the project has marker(s). even though they are "problem" we
711             // don't know their severity. so we loop on them and figure if they
712             // are warnings or errors
713             for (IMarker m : markers) {
714                 int s = m.getAttribute(IMarker.SEVERITY, -1);
715                 if (s == IMarker.SEVERITY_ERROR) {
716                     return true;
717                 }
718             }
719         }
720 
721         // test the referenced projects if needed.
722         if (includeReferencedProjects) {
723             List<IProject> projects = getReferencedProjects(project);
724 
725             for (IProject p : projects) {
726                 if (hasError(p, false)) {
727                     return true;
728                 }
729             }
730         }
731 
732         return false;
733     }
734 
735     /**
736      * Saves a String property into the persistent storage of a resource.
737      * @param resource The resource into which the string value is saved.
738      * @param propertyName the name of the property. The id of the plug-in is added to this string.
739      * @param value the value to save
740      * @return true if the save succeeded.
741      */
saveStringProperty(IResource resource, String propertyName, String value)742     public static boolean saveStringProperty(IResource resource, String propertyName,
743             String value) {
744         QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName);
745 
746         try {
747             resource.setPersistentProperty(qname, value);
748         } catch (CoreException e) {
749             return false;
750         }
751 
752         return true;
753     }
754 
755     /**
756      * Loads a String property from the persistent storage of a resource.
757      * @param resource The resource from which the string value is loaded.
758      * @param propertyName the name of the property. The id of the plug-in is added to this string.
759      * @return the property value or null if it was not found.
760      */
loadStringProperty(IResource resource, String propertyName)761     public static String loadStringProperty(IResource resource, String propertyName) {
762         QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName);
763 
764         try {
765             String value = resource.getPersistentProperty(qname);
766             return value;
767         } catch (CoreException e) {
768             return null;
769         }
770     }
771 
772     /**
773      * Saves a property into the persistent storage of a resource.
774      * @param resource The resource into which the boolean value is saved.
775      * @param propertyName the name of the property. The id of the plug-in is added to this string.
776      * @param value the value to save
777      * @return true if the save succeeded.
778      */
saveBooleanProperty(IResource resource, String propertyName, boolean value)779     public static boolean saveBooleanProperty(IResource resource, String propertyName,
780             boolean value) {
781         return saveStringProperty(resource, propertyName, Boolean.toString(value));
782     }
783 
784     /**
785      * Loads a boolean property from the persistent storage of a resource.
786      * @param resource The resource from which the boolean value is loaded.
787      * @param propertyName the name of the property. The id of the plug-in is added to this string.
788      * @param defaultValue The default value to return if the property was not found.
789      * @return the property value or the default value if the property was not found.
790      */
loadBooleanProperty(IResource resource, String propertyName, boolean defaultValue)791     public static boolean loadBooleanProperty(IResource resource, String propertyName,
792             boolean defaultValue) {
793         String value = loadStringProperty(resource, propertyName);
794         if (value != null) {
795             return Boolean.parseBoolean(value);
796         }
797 
798         return defaultValue;
799     }
800 
loadBooleanProperty(IResource resource, String propertyName)801     public static Boolean loadBooleanProperty(IResource resource, String propertyName) {
802         String value = loadStringProperty(resource, propertyName);
803         if (value != null) {
804             return Boolean.valueOf(value);
805         }
806 
807         return null;
808     }
809 
810     /**
811      * Saves the path of a resource into the persistent storage of a resource.
812      * @param resource The resource into which the resource path is saved.
813      * @param propertyName the name of the property. The id of the plug-in is added to this string.
814      * @param value The resource to save. It's its path that is actually stored. If null, an
815      *      empty string is stored.
816      * @return true if the save succeeded
817      */
saveResourceProperty(IResource resource, String propertyName, IResource value)818     public static boolean saveResourceProperty(IResource resource, String propertyName,
819             IResource value) {
820         if (value != null) {
821             IPath iPath = value.getFullPath();
822             return saveStringProperty(resource, propertyName, iPath.toString());
823         }
824 
825         return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$
826     }
827 
828     /**
829      * Loads the path of a resource from the persistent storage of a resource, and returns the
830      * corresponding IResource object.
831      * @param resource The resource from which the resource path is loaded.
832      * @param propertyName the name of the property. The id of the plug-in is added to this string.
833      * @return The corresponding IResource object (or children interface) or null
834      */
loadResourceProperty(IResource resource, String propertyName)835     public static IResource loadResourceProperty(IResource resource, String propertyName) {
836         String value = loadStringProperty(resource, propertyName);
837 
838         if (value != null && value.length() > 0) {
839             return ResourcesPlugin.getWorkspace().getRoot().findMember(new Path(value));
840         }
841 
842         return null;
843     }
844 
845     /**
846      * Returns the list of referenced project that are opened and Java projects.
847      * @param project
848      * @return a new list object containing the opened referenced java project.
849      * @throws CoreException
850      */
getReferencedProjects(IProject project)851     public static List<IProject> getReferencedProjects(IProject project) throws CoreException {
852         IProject[] projects = project.getReferencedProjects();
853 
854         ArrayList<IProject> list = new ArrayList<IProject>();
855 
856         for (IProject p : projects) {
857             if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {
858                 list.add(p);
859             }
860         }
861 
862         return list;
863     }
864 
865 
866     /**
867      * Checks a Java project compiler level option against a list of supported versions.
868      * @param optionValue the Compiler level option.
869      * @return true if the option value is supported.
870      */
checkCompliance(@onNull IJavaProject project, String optionValue)871     private static boolean checkCompliance(@NonNull IJavaProject project, String optionValue) {
872         for (String s : AdtConstants.COMPILER_COMPLIANCE) {
873             if (s != null && s.equals(optionValue)) {
874                 return true;
875             }
876         }
877 
878         if (JavaCore.VERSION_1_7.equals(optionValue)) {
879             // Requires API 19 and buildTools 19
880             Sdk currentSdk = Sdk.getCurrent();
881             if (currentSdk != null) {
882                 IProject p = project.getProject();
883                 IAndroidTarget target = currentSdk.getTarget(p);
884                 if (target == null || target.getVersion().getApiLevel() < 19) {
885                     return false;
886                 }
887 
888                 ProjectState projectState = Sdk.getProjectState(p);
889                 if (projectState != null) {
890                     BuildToolInfo buildToolInfo = projectState.getBuildToolInfo();
891                     if (buildToolInfo == null) {
892                         buildToolInfo = currentSdk.getLatestBuildTool();
893                     }
894                     if (buildToolInfo == null || buildToolInfo.getRevision().getMajor() < 19) {
895                         return false;
896                     }
897                 }
898 
899                 return true;
900             }
901         }
902 
903         return false;
904     }
905 
906     /**
907      * Returns the apk filename for the given project
908      * @param project The project.
909      * @param config An optional config name. Can be null.
910      */
getApkFilename(IProject project, String config)911     public static String getApkFilename(IProject project, String config) {
912         if (config != null) {
913             return project.getName() + "-" + config + SdkConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$
914         }
915 
916         return project.getName() + SdkConstants.DOT_ANDROID_PACKAGE;
917     }
918 
919     /**
920      * Find the list of projects on which this JavaProject is dependent on at the compilation level.
921      *
922      * @param javaProject Java project that we are looking for the dependencies.
923      * @return A list of Java projects for which javaProject depend on.
924      * @throws JavaModelException
925      */
getAndroidProjectDependencies(IJavaProject javaProject)926     public static List<IJavaProject> getAndroidProjectDependencies(IJavaProject javaProject)
927         throws JavaModelException {
928         String[] requiredProjectNames = javaProject.getRequiredProjectNames();
929 
930         // Go from java project name to JavaProject name
931         IJavaModel javaModel = javaProject.getJavaModel();
932 
933         // loop through all dependent projects and keep only those that are Android projects
934         List<IJavaProject> projectList = new ArrayList<IJavaProject>(requiredProjectNames.length);
935         for (String javaProjectName : requiredProjectNames) {
936             IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName);
937 
938             //Verify that the project has also the Android Nature
939             try {
940                 if (!androidJavaProject.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) {
941                     continue;
942                 }
943             } catch (CoreException e) {
944                 continue;
945             }
946 
947             projectList.add(androidJavaProject);
948         }
949 
950         return projectList;
951     }
952 
953     /**
954      * Returns the android package file as an IFile object for the specified
955      * project.
956      * @param project The project
957      * @return The android package as an IFile object or null if not found.
958      */
getApplicationPackage(IProject project)959     public static IFile getApplicationPackage(IProject project) {
960         // get the output folder
961         IFolder outputLocation = BaseProjectHelper.getAndroidOutputFolder(project);
962 
963         if (outputLocation == null) {
964             AdtPlugin.printErrorToConsole(project,
965                     "Failed to get the output location of the project. Check build path properties"
966                     );
967             return null;
968         }
969 
970 
971         // get the package path
972         String packageName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE;
973         IResource r = outputLocation.findMember(packageName);
974 
975         // check the package is present
976         if (r instanceof IFile && r.exists()) {
977             return (IFile)r;
978         }
979 
980         String msg = String.format("Could not find %1$s!", packageName);
981         AdtPlugin.printErrorToConsole(project, msg);
982 
983         return null;
984     }
985 
986     /**
987      * Returns an {@link IFile} object representing the manifest for the given project.
988      *
989      * @param project The project containing the manifest file.
990      * @return An IFile object pointing to the manifest or null if the manifest
991      *         is missing.
992      */
getManifest(IProject project)993     public static IFile getManifest(IProject project) {
994         IResource r = project.findMember(AdtConstants.WS_SEP
995                 + SdkConstants.FN_ANDROID_MANIFEST_XML);
996 
997         if (r == null || r.exists() == false || (r instanceof IFile) == false) {
998             return null;
999         }
1000         return (IFile) r;
1001     }
1002 
1003     /**
1004      * Does a full release build of the application, including the libraries. Do not build the
1005      * package.
1006      *
1007      * @param project The project to be built.
1008      * @param monitor A eclipse runtime progress monitor to be updated by the builders.
1009      * @throws CoreException
1010      */
1011     @SuppressWarnings("unchecked")
compileInReleaseMode(IProject project, IProgressMonitor monitor)1012     public static void compileInReleaseMode(IProject project, IProgressMonitor monitor)
1013             throws CoreException {
1014         compileInReleaseMode(project, true /*includeDependencies*/, monitor);
1015     }
1016 
1017     /**
1018      * Does a full release build of the application, including the libraries. Do not build the
1019      * package.
1020      *
1021      * @param project The project to be built.
1022      * @param monitor A eclipse runtime progress monitor to be updated by the builders.
1023      * @throws CoreException
1024      */
1025     @SuppressWarnings("unchecked")
compileInReleaseMode(IProject project, boolean includeDependencies, IProgressMonitor monitor)1026     private static void compileInReleaseMode(IProject project, boolean includeDependencies,
1027             IProgressMonitor monitor)
1028             throws CoreException {
1029 
1030         if (includeDependencies) {
1031             ProjectState projectState = Sdk.getProjectState(project);
1032 
1033             // this gives us all the library projects, direct and indirect dependencies,
1034             // so no need to run this method recursively.
1035             List<IProject> libraries = projectState.getFullLibraryProjects();
1036 
1037             // build dependencies in reverse order to prevent libraries being rebuilt
1038             // due to refresh of other libraries (they would be compiled in the wrong mode).
1039             for (int i = libraries.size() - 1 ; i >= 0 ; i--) {
1040                 IProject lib = libraries.get(i);
1041                 compileInReleaseMode(lib, false /*includeDependencies*/, monitor);
1042 
1043                 // force refresh of the dependency.
1044                 lib.refreshLocal(IResource.DEPTH_INFINITE, monitor);
1045             }
1046         }
1047 
1048         // do a full build on all the builders to guarantee that the builders are called.
1049         // (Eclipse does an optimization where builders are not called if there aren't any
1050         // deltas).
1051 
1052         ICommand[] commands = project.getDescription().getBuildSpec();
1053         for (ICommand command : commands) {
1054             String name = command.getBuilderName();
1055             if (PreCompilerBuilder.ID.equals(name)) {
1056                 Map newArgs = new HashMap();
1057                 newArgs.put(PreCompilerBuilder.RELEASE_REQUESTED, "");
1058                 if (command.getArguments() != null) {
1059                     newArgs.putAll(command.getArguments());
1060                 }
1061 
1062                 project.build(IncrementalProjectBuilder.FULL_BUILD,
1063                         PreCompilerBuilder.ID, newArgs, monitor);
1064             } else if (PostCompilerBuilder.ID.equals(name)) {
1065                 if (includeDependencies == false) {
1066                     // this is a library, we need to build it!
1067                     project.build(IncrementalProjectBuilder.FULL_BUILD, name,
1068                             command.getArguments(), monitor);
1069                 }
1070             } else {
1071 
1072                 project.build(IncrementalProjectBuilder.FULL_BUILD, name,
1073                         command.getArguments(), monitor);
1074             }
1075         }
1076     }
1077 
1078     /**
1079      * Force building the project and all its dependencies.
1080      *
1081      * @param project the project to build
1082      * @param kind the build kind
1083      * @param monitor
1084      * @throws CoreException
1085      */
buildWithDeps(IProject project, int kind, IProgressMonitor monitor)1086     public static void buildWithDeps(IProject project, int kind, IProgressMonitor monitor)
1087             throws CoreException {
1088         // Get list of projects that we depend on
1089         ProjectState projectState = Sdk.getProjectState(project);
1090 
1091         // this gives us all the library projects, direct and indirect dependencies,
1092         // so no need to run this method recursively.
1093         List<IProject> libraries = projectState.getFullLibraryProjects();
1094 
1095         // build dependencies in reverse order to prevent libraries being rebuilt
1096         // due to refresh of other libraries (they would be compiled in the wrong mode).
1097         for (int i = libraries.size() - 1 ; i >= 0 ; i--) {
1098             IProject lib = libraries.get(i);
1099             lib.build(kind, monitor);
1100             lib.refreshLocal(IResource.DEPTH_INFINITE, monitor);
1101         }
1102 
1103         project.build(kind, monitor);
1104     }
1105 
1106 
1107     /**
1108      * Build project incrementally, including making the final packaging even if it is disabled
1109      * by default.
1110      *
1111      * @param project The project to be built.
1112      * @param monitor A eclipse runtime progress monitor to be updated by the builders.
1113      * @throws CoreException
1114      */
doFullIncrementalDebugBuild(IProject project, IProgressMonitor monitor)1115     public static void doFullIncrementalDebugBuild(IProject project, IProgressMonitor monitor)
1116             throws CoreException {
1117         // Get list of projects that we depend on
1118         List<IJavaProject> androidProjectList = new ArrayList<IJavaProject>();
1119         try {
1120             androidProjectList = getAndroidProjectDependencies(
1121                                     BaseProjectHelper.getJavaProject(project));
1122         } catch (JavaModelException e) {
1123             AdtPlugin.printErrorToConsole(project, e);
1124         }
1125         // Recursively build dependencies
1126         for (IJavaProject dependency : androidProjectList) {
1127             doFullIncrementalDebugBuild(dependency.getProject(), monitor);
1128         }
1129 
1130         // Do an incremental build to pick up all the deltas
1131         project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
1132 
1133         // If the preferences indicate not to use post compiler optimization
1134         // then the incremental build will have done everything necessary, otherwise,
1135         // we have to run the final builder manually (if requested).
1136         if (AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) {
1137             // Create the map to pass to the PostC builder
1138             Map<String, String> args = new TreeMap<String, String>();
1139             args.put(PostCompilerBuilder.POST_C_REQUESTED, ""); //$NON-NLS-1$
1140 
1141             // call the post compiler manually, forcing FULL_BUILD otherwise Eclipse won't
1142             // call the builder since the delta is empty.
1143             project.build(IncrementalProjectBuilder.FULL_BUILD,
1144                           PostCompilerBuilder.ID, args, monitor);
1145         }
1146 
1147         // because the post compiler builder does a delayed refresh due to
1148         // library not picking the refresh up if it's done during the build,
1149         // we want to force a refresh here as this call is generally asking for
1150         // a build to use the apk right after the call.
1151         project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
1152     }
1153 }
1154