Home | History | Annotate | Download | only in sdk
      1 /*
      2  * Copyright (C) 2008 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.sdk;
     18 
     19 import static com.android.SdkConstants.DOT_XML;
     20 import static com.android.SdkConstants.EXT_JAR;
     21 import static com.android.SdkConstants.FD_RES;
     22 
     23 import com.android.SdkConstants;
     24 import com.android.annotations.NonNull;
     25 import com.android.annotations.Nullable;
     26 import com.android.ddmlib.IDevice;
     27 import com.android.ide.common.rendering.LayoutLibrary;
     28 import com.android.ide.common.sdk.LoadStatus;
     29 import com.android.ide.eclipse.adt.AdtConstants;
     30 import com.android.ide.eclipse.adt.AdtPlugin;
     31 import com.android.ide.eclipse.adt.internal.build.DexWrapper;
     32 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
     33 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     34 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     35 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
     36 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     37 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     38 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     39 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
     40 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
     41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
     42 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
     43 import com.android.io.StreamException;
     44 import com.android.prefs.AndroidLocation.AndroidLocationException;
     45 import com.android.sdklib.AndroidVersion;
     46 import com.android.sdklib.BuildToolInfo;
     47 import com.android.sdklib.IAndroidTarget;
     48 import com.android.sdklib.SdkManager;
     49 import com.android.sdklib.devices.DeviceManager;
     50 import com.android.sdklib.internal.avd.AvdManager;
     51 import com.android.sdklib.internal.project.ProjectProperties;
     52 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
     53 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
     54 import com.android.sdklib.repository.FullRevision;
     55 import com.android.utils.ILogger;
     56 import com.google.common.collect.Maps;
     57 
     58 import org.eclipse.core.resources.IFile;
     59 import org.eclipse.core.resources.IFolder;
     60 import org.eclipse.core.resources.IMarker;
     61 import org.eclipse.core.resources.IMarkerDelta;
     62 import org.eclipse.core.resources.IProject;
     63 import org.eclipse.core.resources.IResource;
     64 import org.eclipse.core.resources.IResourceDelta;
     65 import org.eclipse.core.resources.IncrementalProjectBuilder;
     66 import org.eclipse.core.resources.ResourcesPlugin;
     67 import org.eclipse.core.runtime.CoreException;
     68 import org.eclipse.core.runtime.IPath;
     69 import org.eclipse.core.runtime.IProgressMonitor;
     70 import org.eclipse.core.runtime.IStatus;
     71 import org.eclipse.core.runtime.QualifiedName;
     72 import org.eclipse.core.runtime.Status;
     73 import org.eclipse.core.runtime.jobs.Job;
     74 import org.eclipse.jdt.core.IJavaProject;
     75 import org.eclipse.jdt.core.JavaCore;
     76 import org.eclipse.jdt.core.JavaModelException;
     77 import org.eclipse.jface.preference.IPreferenceStore;
     78 import org.eclipse.ui.IEditorDescriptor;
     79 import org.eclipse.ui.IEditorInput;
     80 import org.eclipse.ui.IEditorPart;
     81 import org.eclipse.ui.IEditorReference;
     82 import org.eclipse.ui.IFileEditorInput;
     83 import org.eclipse.ui.IWorkbenchPage;
     84 import org.eclipse.ui.IWorkbenchPartSite;
     85 import org.eclipse.ui.IWorkbenchWindow;
     86 import org.eclipse.ui.PartInitException;
     87 import org.eclipse.ui.PlatformUI;
     88 import org.eclipse.ui.ide.IDE;
     89 
     90 import java.io.File;
     91 import java.io.IOException;
     92 import java.net.MalformedURLException;
     93 import java.net.URL;
     94 import java.util.ArrayList;
     95 import java.util.Arrays;
     96 import java.util.Collection;
     97 import java.util.HashMap;
     98 import java.util.HashSet;
     99 import java.util.List;
    100 import java.util.Map;
    101 import java.util.Map.Entry;
    102 import java.util.Set;
    103 import java.util.concurrent.atomic.AtomicBoolean;
    104 
    105 /**
    106  * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
    107  * at the same time.
    108  *
    109  * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
    110  * the Sdk object.
    111  *
    112  * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
    113  */
    114 public final class Sdk  {
    115     private final static boolean DEBUG = false;
    116 
    117     private final static Object LOCK = new Object();
    118 
    119     private static Sdk sCurrentSdk = null;
    120 
    121     /**
    122      * Map associating {@link IProject} and their state {@link ProjectState}.
    123      * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
    124      */
    125     private final static HashMap<IProject, ProjectState> sProjectStateMap =
    126             new HashMap<IProject, ProjectState>();
    127 
    128     /**
    129      * Data bundled using during the load of Target data.
    130      * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
    131      * to compile before the loading was finished. Those projects will be recompiled
    132      * at the end of the loading.
    133      */
    134     private final static class TargetLoadBundle {
    135         LoadStatus status;
    136         final HashSet<IJavaProject> projectsToReload = new HashSet<IJavaProject>();
    137     }
    138 
    139     private final SdkManager mManager;
    140     private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap();
    141     private final AvdManager mAvdManager;
    142     private final DeviceManager mDeviceManager;
    143 
    144     /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
    145     private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
    146         new HashMap<IAndroidTarget, AndroidTargetData>();
    147     /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
    148     private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
    149         new HashMap<IAndroidTarget, TargetLoadBundle>();
    150 
    151     /**
    152      * If true the target data will never load anymore. The only way to reload them is to
    153      * completely reload the SDK with {@link #loadSdk(String)}
    154      */
    155     private boolean mDontLoadTargetData = false;
    156 
    157     private final String mDocBaseUrl;
    158 
    159     /**
    160      * Classes implementing this interface will receive notification when targets are changed.
    161      */
    162     public interface ITargetChangeListener {
    163         /**
    164          * Sent when project has its target changed.
    165          */
    166         void onProjectTargetChange(IProject changedProject);
    167 
    168         /**
    169          * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
    170          * or the SDK is changed).
    171          */
    172         void onTargetLoaded(IAndroidTarget target);
    173 
    174         /**
    175          * Called when the base content of the SDK is parsed.
    176          */
    177         void onSdkLoaded();
    178     }
    179 
    180     /**
    181      * Basic abstract implementation of the ITargetChangeListener for the case where both
    182      * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
    183      * use the same code based on a simple test requiring to know the current IProject.
    184      */
    185     public static abstract class TargetChangeListener implements ITargetChangeListener {
    186         /**
    187          * Returns the {@link IProject} associated with the listener.
    188          */
    189         public abstract IProject getProject();
    190 
    191         /**
    192          * Called when the listener needs to take action on the event. This is only called
    193          * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
    194          * match the values received in {@link #onProjectTargetChange(IProject)} and
    195          * {@link #onTargetLoaded(IAndroidTarget)}.
    196          */
    197         public abstract void reload();
    198 
    199         @Override
    200         public void onProjectTargetChange(IProject changedProject) {
    201             if (changedProject != null && changedProject.equals(getProject())) {
    202                 reload();
    203             }
    204         }
    205 
    206         @Override
    207         public void onTargetLoaded(IAndroidTarget target) {
    208             IProject project = getProject();
    209             if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
    210                 reload();
    211             }
    212         }
    213 
    214         @Override
    215         public void onSdkLoaded() {
    216             // do nothing;
    217         }
    218     }
    219 
    220     /**
    221      * Returns the lock object used to synchronize all operations dealing with SDK, targets and
    222      * projects.
    223      */
    224     @NonNull
    225     public static final Object getLock() {
    226         return LOCK;
    227     }
    228 
    229     /**
    230      * Loads an SDK and returns an {@link Sdk} object if success.
    231      * <p/>If the SDK failed to load, it displays an error to the user.
    232      * @param sdkLocation the OS path to the SDK.
    233      */
    234     @Nullable
    235     public static Sdk loadSdk(String sdkLocation) {
    236         synchronized (LOCK) {
    237             if (sCurrentSdk != null) {
    238                 sCurrentSdk.dispose();
    239                 sCurrentSdk = null;
    240             }
    241 
    242             final AtomicBoolean hasWarning = new AtomicBoolean();
    243             final AtomicBoolean hasError = new AtomicBoolean();
    244             final ArrayList<String> logMessages = new ArrayList<String>();
    245             ILogger log = new ILogger() {
    246                 @Override
    247                 public void error(@Nullable Throwable throwable, @Nullable String errorFormat,
    248                         Object... arg) {
    249                     hasError.set(true);
    250                     if (errorFormat != null) {
    251                         logMessages.add(String.format("Error: " + errorFormat, arg));
    252                     }
    253 
    254                     if (throwable != null) {
    255                         logMessages.add(throwable.getMessage());
    256                     }
    257                 }
    258 
    259                 @Override
    260                 public void warning(@NonNull String warningFormat, Object... arg) {
    261                     hasWarning.set(true);
    262                     logMessages.add(String.format("Warning: " + warningFormat, arg));
    263                 }
    264 
    265                 @Override
    266                 public void info(@NonNull String msgFormat, Object... arg) {
    267                     logMessages.add(String.format(msgFormat, arg));
    268                 }
    269 
    270                 @Override
    271                 public void verbose(@NonNull String msgFormat, Object... arg) {
    272                     info(msgFormat, arg);
    273                 }
    274             };
    275 
    276             // get an SdkManager object for the location
    277             SdkManager manager = SdkManager.createManager(sdkLocation, log);
    278             try {
    279                 if (manager == null) {
    280                     hasError.set(true);
    281                 } else {
    282                     // create the AVD Manager
    283                     AvdManager avdManager = null;
    284                     try {
    285                         avdManager = AvdManager.getInstance(manager.getLocalSdk(), log);
    286                     } catch (AndroidLocationException e) {
    287                         log.error(e, "Error parsing the AVDs");
    288                     }
    289                     sCurrentSdk = new Sdk(manager, avdManager);
    290                     return sCurrentSdk;
    291                 }
    292             } finally {
    293                 if (hasError.get() || hasWarning.get()) {
    294                     StringBuilder sb = new StringBuilder(
    295                             String.format("%s when loading the SDK:\n",
    296                                     hasError.get() ? "Error" : "Warning"));
    297                     for (String msg : logMessages) {
    298                         sb.append('\n');
    299                         sb.append(msg);
    300                     }
    301                     if (hasError.get()) {
    302                         AdtPlugin.printErrorToConsole("Android SDK", sb.toString());
    303                         AdtPlugin.displayError("Android SDK", sb.toString());
    304                     } else {
    305                         AdtPlugin.printToConsole("Android SDK", sb.toString());
    306                     }
    307                 }
    308             }
    309             return null;
    310         }
    311     }
    312 
    313     /**
    314      * Returns the current {@link Sdk} object.
    315      */
    316     @Nullable
    317     public static Sdk getCurrent() {
    318         synchronized (LOCK) {
    319             return sCurrentSdk;
    320         }
    321     }
    322 
    323     /**
    324      * Returns the location of the current SDK as an OS path string.
    325      * Guaranteed to be terminated by a platform-specific path separator.
    326      * <p/>
    327      * Due to {@link File} canonicalization, this MAY differ from the string used to initialize
    328      * the SDK path.
    329      *
    330      * @return The SDK OS path or null if no SDK is setup.
    331      * @deprecated Consider using {@link #getSdkFileLocation()} instead.
    332      * @see #getSdkFileLocation()
    333      */
    334     @Deprecated
    335     @Nullable
    336     public String getSdkOsLocation() {
    337         String path = mManager == null ? null : mManager.getLocation();
    338         if (path != null) {
    339             // For backward compatibility make sure it ends with a separator.
    340             // This used to be the case when the SDK Manager was created from a String path
    341             // but now that a File is internally used the trailing dir separator is lost.
    342             if (path.length() > 0 && !path.endsWith(File.separator)) {
    343                 path = path + File.separator;
    344             }
    345         }
    346         return path;
    347     }
    348 
    349     /**
    350      * Returns the location of the current SDK as a {@link File} or null.
    351      *
    352      * @return The SDK OS path or null if no SDK is setup.
    353      */
    354     @Nullable
    355     public File getSdkFileLocation() {
    356         if (mManager == null || mManager.getLocalSdk() == null) {
    357             return null;
    358         }
    359         return mManager.getLocalSdk().getLocation();
    360     }
    361 
    362     /**
    363      * Returns a <em>new</em> {@link SdkManager} that can parse the SDK located
    364      * at the current {@link #getSdkOsLocation()}.
    365      * <p/>
    366      * Implementation detail: The {@link Sdk} has its own internal manager with
    367      * a custom logger which is not designed to be useful for outsiders. Callers
    368      * who need their own {@link SdkManager} for parsing will often want to control
    369      * the logger for their own need.
    370      * <p/>
    371      * This is just a convenient method equivalent to writing:
    372      * <pre>SdkManager.createManager(Sdk.getCurrent().getSdkLocation(), log);</pre>
    373      *
    374      * @param log The logger for the {@link SdkManager}.
    375      * @return A new {@link SdkManager} parsing the same location.
    376      */
    377     public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) {
    378         return SdkManager.createManager(getSdkOsLocation(), log);
    379     }
    380 
    381     /**
    382      * Returns the URL to the local documentation.
    383      * Can return null if no documentation is found in the current SDK.
    384      *
    385      * @return A file:// URL on the local documentation folder if it exists or null.
    386      */
    387     @Nullable
    388     public String getDocumentationBaseUrl() {
    389         return mDocBaseUrl;
    390     }
    391 
    392     /**
    393      * Returns the list of targets that are available in the SDK.
    394      */
    395     public IAndroidTarget[] getTargets() {
    396         return mManager.getTargets();
    397     }
    398 
    399     /**
    400      * Queries the underlying SDK Manager to check whether the platforms or addons
    401      * directories have changed on-disk. Does not reload the SDK.
    402      * <p/>
    403      * This is a quick test based on the presence of the directories, their timestamps
    404      * and a quick checksum of the source.properties files. It's possible to have
    405      * false positives (e.g. if a file is manually modified in a platform) or false
    406      * negatives (e.g. if a platform data file is changed manually in a 2nd level
    407      * directory without altering the source.properties.)
    408      */
    409     public boolean haveTargetsChanged() {
    410         return mManager.hasChanged();
    411     }
    412 
    413     /**
    414      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
    415      *
    416      * @param hash the {@link IAndroidTarget} hash string.
    417      * @return The matching {@link IAndroidTarget} or null.
    418      */
    419     @Nullable
    420     public IAndroidTarget getTargetFromHashString(@NonNull String hash) {
    421         return mManager.getTargetFromHashString(hash);
    422     }
    423 
    424     @Nullable
    425     public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) {
    426         if (buildToolVersion != null) {
    427             try {
    428                 return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion));
    429             } catch (Exception e) {
    430                 // ignore, return null below.
    431             }
    432         }
    433 
    434         return null;
    435     }
    436 
    437     @Nullable
    438     public BuildToolInfo getLatestBuildTool() {
    439         return mManager.getLatestBuildTool();
    440     }
    441 
    442     /**
    443      * Initializes a new project with a target. This creates the <code>project.properties</code>
    444      * file.
    445      * @param project the project to initialize
    446      * @param target the project's target.
    447      * @throws IOException if creating the file failed in any way.
    448      * @throws StreamException if processing the project property file fails
    449      */
    450     public void initProject(@Nullable IProject project, @Nullable IAndroidTarget target)
    451             throws IOException, StreamException {
    452         if (project == null || target == null) {
    453             return;
    454         }
    455 
    456         synchronized (LOCK) {
    457             // check if there's already a state?
    458             ProjectState state = getProjectState(project);
    459 
    460             ProjectPropertiesWorkingCopy properties = null;
    461 
    462             if (state != null) {
    463                 properties = state.getProperties().makeWorkingCopy();
    464             }
    465 
    466             if (properties == null) {
    467                 IPath location = project.getLocation();
    468                 if (location == null) {  // can return null when the project is being deleted.
    469                     // do nothing and return null;
    470                     return;
    471                 }
    472 
    473                 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT);
    474             }
    475 
    476             // save the target hash string in the project persistent property
    477             properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    478             properties.save();
    479         }
    480     }
    481 
    482     /**
    483      * Returns the {@link ProjectState} object associated with a given project.
    484      * <p/>
    485      * This method is the only way to properly get the project's {@link ProjectState}
    486      * If the project has not yet been loaded, then it is loaded.
    487      * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
    488      * objects, and therefore is static.
    489      * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
    490      * are replaced.
    491      * @param project the request project
    492      * @return the ProjectState for the project.
    493      */
    494     @Nullable
    495     @SuppressWarnings("deprecation")
    496     public static ProjectState getProjectState(IProject project) {
    497         if (project == null) {
    498             return null;
    499         }
    500 
    501         synchronized (LOCK) {
    502             ProjectState state = sProjectStateMap.get(project);
    503             if (state == null) {
    504                 // load the project.properties from the project folder.
    505                 IPath location = project.getLocation();
    506                 if (location == null) {  // can return null when the project is being deleted.
    507                     // do nothing and return null;
    508                     return null;
    509                 }
    510 
    511                 String projectLocation = location.toOSString();
    512 
    513                 ProjectProperties properties = ProjectProperties.load(projectLocation,
    514                         PropertyType.PROJECT);
    515                 if (properties == null) {
    516                     // legacy support: look for default.properties and rename it if needed.
    517                     properties = ProjectProperties.load(projectLocation,
    518                             PropertyType.LEGACY_DEFAULT);
    519 
    520                     if (properties == null) {
    521                         AdtPlugin.log(IStatus.ERROR,
    522                                 "Failed to load properties file for project '%s'",
    523                                 project.getName());
    524                         return null;
    525                     } else {
    526                         //legacy mode.
    527                         // get a working copy with the new type "project"
    528                         ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy(
    529                                 PropertyType.PROJECT);
    530                         // and save it
    531                         try {
    532                             wc.save();
    533 
    534                             // delete the old file.
    535                             ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT);
    536 
    537                             // make sure to use the new properties
    538                             properties = ProjectProperties.load(projectLocation,
    539                                     PropertyType.PROJECT);
    540                         } catch (Exception e) {
    541                             AdtPlugin.log(IStatus.ERROR,
    542                                     "Failed to rename properties file to %1$s for project '%s2$'",
    543                                     PropertyType.PROJECT.getFilename(), project.getName());
    544                         }
    545                     }
    546                 }
    547 
    548                 state = new ProjectState(project, properties);
    549                 sProjectStateMap.put(project, state);
    550 
    551                 // try to resolve the target
    552                 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
    553                     sCurrentSdk.loadTargetAndBuildTools(state);
    554                 }
    555             }
    556 
    557             return state;
    558         }
    559     }
    560 
    561     /**
    562      * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
    563      */
    564     @Nullable
    565     public IAndroidTarget getTarget(IProject project) {
    566         if (project == null) {
    567             return null;
    568         }
    569 
    570         ProjectState state = getProjectState(project);
    571         if (state != null) {
    572             return state.getTarget();
    573         }
    574 
    575         return null;
    576     }
    577 
    578     /**
    579      * Loads the {@link IAndroidTarget} and BuildTools for a given project.
    580      * <p/>This method will get the target hash string from the project properties, and resolve
    581      * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
    582      * @param state the state representing the project to load.
    583      * @return the target that was loaded.
    584      */
    585     @Nullable
    586     public IAndroidTarget loadTargetAndBuildTools(ProjectState state) {
    587         IAndroidTarget target = null;
    588         if (state != null) {
    589             String hash = state.getTargetHashString();
    590             if (hash != null) {
    591                 state.setTarget(target = getTargetFromHashString(hash));
    592             }
    593 
    594             String markerMessage = null;
    595             String buildToolInfoVersion = state.getBuildToolInfoVersion();
    596             if (buildToolInfoVersion != null) {
    597                 BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion);
    598 
    599                 if (buildToolsInfo != null) {
    600                     state.setBuildToolInfo(buildToolsInfo);
    601                 } else {
    602                     markerMessage = String.format("Unable to resolve %s property value '%s'",
    603                                         ProjectProperties.PROPERTY_BUILD_TOOLS,
    604                                         buildToolInfoVersion);
    605                 }
    606             } else {
    607                 // this is ok, we'll use the latest one automatically.
    608                 state.setBuildToolInfo(null);
    609             }
    610 
    611             handleBuildToolsMarker(state.getProject(), markerMessage);
    612         }
    613 
    614         return target;
    615     }
    616 
    617     /**
    618      * Adds or edit a build tools marker from the given project. This is done through a Job.
    619      * @param project the project
    620      * @param markerMessage the message. if null the marker is removed.
    621      */
    622     private void handleBuildToolsMarker(final IProject project, final String markerMessage) {
    623         Job markerJob = new Job("Android SDK: Build Tools Marker") {
    624             @Override
    625             protected IStatus run(IProgressMonitor monitor) {
    626                 try {
    627                     if (project.isAccessible()) {
    628                         // always delete existing marker first
    629                         project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true,
    630                                 IResource.DEPTH_ZERO);
    631 
    632                         // add the new one if needed.
    633                         if (markerMessage != null) {
    634                             BaseProjectHelper.markProject(project,
    635                                     AdtConstants.MARKER_BUILD_TOOLS,
    636                                     markerMessage, IMarker.SEVERITY_ERROR,
    637                                     IMarker.PRIORITY_HIGH);
    638                         }
    639                     }
    640                 } catch (CoreException e2) {
    641                     AdtPlugin.log(e2, null);
    642                     // Don't return e2.getStatus(); the job control will then produce
    643                     // a popup with this error, which isn't very interesting for the
    644                     // user.
    645                 }
    646 
    647                 return Status.OK_STATUS;
    648             }
    649         };
    650 
    651         // build jobs are run after other interactive jobs
    652         markerJob.setPriority(Job.BUILD);
    653         markerJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
    654         markerJob.schedule();
    655     }
    656 
    657     /**
    658      * Checks and loads (if needed) the data for a given target.
    659      * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
    660      * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
    661      * <p/>An optional project as second parameter can be given to be recompiled once the target
    662      * data is finished loading.
    663      * <p/>The return value is non-null only if the target data has already been loaded (and in this
    664      * case is the status of the load operation)
    665      * @param target the target to load.
    666      * @param project an optional project to be recompiled when the target data is loaded.
    667      * If the target is already loaded, nothing happens.
    668      * @return The load status if the target data is already loaded.
    669      */
    670     @NonNull
    671     public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
    672         boolean loadData = false;
    673 
    674         synchronized (LOCK) {
    675             if (mDontLoadTargetData) {
    676                 return LoadStatus.FAILED;
    677             }
    678 
    679             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    680             if (bundle == null) {
    681                 bundle = new TargetLoadBundle();
    682                 mTargetDataStatusMap.put(target,bundle);
    683 
    684                 // set status to loading
    685                 bundle.status = LoadStatus.LOADING;
    686 
    687                 // add project to bundle
    688                 if (project != null) {
    689                     bundle.projectsToReload.add(project);
    690                 }
    691 
    692                 // and set the flag to start the loading below
    693                 loadData = true;
    694             } else if (bundle.status == LoadStatus.LOADING) {
    695                 // add project to bundle
    696                 if (project != null) {
    697                     bundle.projectsToReload.add(project);
    698                 }
    699 
    700                 return bundle.status;
    701             } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
    702                 return bundle.status;
    703             }
    704         }
    705 
    706         if (loadData) {
    707             Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
    708                 @Override
    709                 protected IStatus run(IProgressMonitor monitor) {
    710                     AdtPlugin plugin = AdtPlugin.getDefault();
    711                     try {
    712                         IStatus status = new AndroidTargetParser(target).run(monitor);
    713 
    714                         IJavaProject[] javaProjectArray = null;
    715 
    716                         synchronized (LOCK) {
    717                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    718 
    719                             if (status.getCode() != IStatus.OK) {
    720                                 bundle.status = LoadStatus.FAILED;
    721                                 bundle.projectsToReload.clear();
    722                             } else {
    723                                 bundle.status = LoadStatus.LOADED;
    724 
    725                                 // Prepare the array of project to recompile.
    726                                 // The call is done outside of the synchronized block.
    727                                 javaProjectArray = bundle.projectsToReload.toArray(
    728                                         new IJavaProject[bundle.projectsToReload.size()]);
    729 
    730                                 // and update the UI of the editors that depend on the target data.
    731                                 plugin.updateTargetListeners(target);
    732                             }
    733                         }
    734 
    735                         if (javaProjectArray != null) {
    736                             ProjectHelper.updateProjects(javaProjectArray);
    737                         }
    738 
    739                         return status;
    740                     } catch (Throwable t) {
    741                         synchronized (LOCK) {
    742                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    743                             bundle.status = LoadStatus.FAILED;
    744                         }
    745 
    746                         AdtPlugin.log(t, "Exception in checkAndLoadTargetData.");    //$NON-NLS-1$
    747                         String message = String.format("Parsing Data for %1$s failed",  target.hashString());
    748                         if (t instanceof UnsupportedClassVersionError) {
    749                             message = "To use this platform, run Eclipse with JDK 7 or later. (" + message + ")";
    750                         }
    751                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, t);
    752                     }
    753                 }
    754             };
    755             job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
    756             job.setRule(ResourcesPlugin.getWorkspace().getRoot());
    757             job.schedule();
    758         }
    759 
    760         // The only way to go through here is when the loading starts through the Job.
    761         // Therefore the current status of the target is LOADING.
    762         return LoadStatus.LOADING;
    763     }
    764 
    765     /**
    766      * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
    767      */
    768     @Nullable
    769     public AndroidTargetData getTargetData(IAndroidTarget target) {
    770         synchronized (LOCK) {
    771             return mTargetDataMap.get(target);
    772         }
    773     }
    774 
    775     /**
    776      * Return the {@link AndroidTargetData} for a given {@link IProject}.
    777      */
    778     @Nullable
    779     public AndroidTargetData getTargetData(IProject project) {
    780         synchronized (LOCK) {
    781             IAndroidTarget target = getTarget(project);
    782             if (target != null) {
    783                 return getTargetData(target);
    784             }
    785         }
    786 
    787         return null;
    788     }
    789 
    790     /**
    791      * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
    792      * loaded properly, then this will return <code>null</code>.
    793      */
    794     @Nullable
    795     public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) {
    796         if (buildToolInfo == null) {
    797             return null;
    798         }
    799         synchronized (LOCK) {
    800             String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
    801             DexWrapper dexWrapper = mDexWrappers.get(dexLocation);
    802 
    803             if (dexWrapper == null) {
    804                 // load DX.
    805                 dexWrapper = new DexWrapper();
    806                 IStatus res = dexWrapper.loadDex(dexLocation);
    807                 if (res != Status.OK_STATUS) {
    808                     AdtPlugin.log(null, res.getMessage());
    809                     dexWrapper = null;
    810                 } else {
    811                     mDexWrappers.put(dexLocation, dexWrapper);
    812                 }
    813             }
    814 
    815             return dexWrapper;
    816         }
    817     }
    818 
    819     public void unloadDexWrappers() {
    820         synchronized (LOCK) {
    821             for (DexWrapper wrapper : mDexWrappers.values()) {
    822                 wrapper.unload();
    823             }
    824             mDexWrappers.clear();
    825         }
    826     }
    827 
    828     /**
    829      * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
    830      * be <code>null</code>.
    831      */
    832     @Nullable
    833     public AvdManager getAvdManager() {
    834         return mAvdManager;
    835     }
    836 
    837     @Nullable
    838     public static AndroidVersion getDeviceVersion(@NonNull IDevice device) {
    839         try {
    840             Map<String, String> props = device.getProperties();
    841             String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
    842             if (apiLevel == null) {
    843                 return null;
    844             }
    845 
    846             return new AndroidVersion(Integer.parseInt(apiLevel),
    847                     props.get((IDevice.PROP_BUILD_CODENAME)));
    848         } catch (NumberFormatException e) {
    849             return null;
    850         }
    851     }
    852 
    853     @NonNull
    854     public DeviceManager getDeviceManager() {
    855         return mDeviceManager;
    856     }
    857 
    858     /**
    859      * Returns a list of {@link ProjectState} representing projects depending, directly or
    860      * indirectly on a given library project.
    861      * @param project the library project.
    862      * @return a possibly empty list of ProjectState.
    863      */
    864     @NonNull
    865     public static Set<ProjectState> getMainProjectsFor(IProject project) {
    866         synchronized (LOCK) {
    867             // first get the project directly depending on this.
    868             Set<ProjectState> list = new HashSet<ProjectState>();
    869 
    870             // loop on all project and see if ProjectState.getLibrary returns a non null
    871             // project.
    872             for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
    873                 if (project != entry.getKey()) {
    874                     LibraryState library = entry.getValue().getLibrary(project);
    875                     if (library != null) {
    876                         list.add(entry.getValue());
    877                     }
    878                 }
    879             }
    880 
    881             // now look for projects depending on the projects directly depending on the library.
    882             HashSet<ProjectState> result = new HashSet<ProjectState>(list);
    883             for (ProjectState p : list) {
    884                 if (p.isLibrary()) {
    885                     Set<ProjectState> set = getMainProjectsFor(p.getProject());
    886                     result.addAll(set);
    887                 }
    888             }
    889 
    890             return result;
    891         }
    892     }
    893 
    894     /**
    895      * Unload the SDK's target data.
    896      *
    897      * If <var>preventReload</var>, this effect is final until the SDK instance is changed
    898      * through {@link #loadSdk(String)}.
    899      *
    900      * The goal is to unload the targets to be able to replace existing targets with new ones,
    901      * before calling {@link #loadSdk(String)} to fully reload the SDK.
    902      *
    903      * @param preventReload prevent the data from being loaded again for the remaining live of
    904      *   this {@link Sdk} instance.
    905      */
    906     public void unloadTargetData(boolean preventReload) {
    907         synchronized (LOCK) {
    908             mDontLoadTargetData = preventReload;
    909 
    910             // dispose of the target data.
    911             for (AndroidTargetData data : mTargetDataMap.values()) {
    912                 data.dispose();
    913             }
    914 
    915             mTargetDataMap.clear();
    916         }
    917     }
    918 
    919     private Sdk(SdkManager manager, AvdManager avdManager) {
    920         mManager = manager;
    921         mAvdManager = avdManager;
    922 
    923         // listen to projects closing
    924         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    925         // need to register the resource event listener first because the project listener
    926         // is called back during registration with project opened in the workspace.
    927         monitor.addResourceEventListener(mResourceEventListener);
    928         monitor.addProjectListener(mProjectListener);
    929         monitor.addFileListener(mFileListener,
    930                 IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED);
    931 
    932         // pre-compute some paths
    933         mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() +
    934                 SdkConstants.OS_SDK_DOCS_FOLDER);
    935 
    936         mDeviceManager = DeviceManager.createInstance(manager.getLocalSdk().getLocation(),
    937                                                       AdtPlugin.getDefault());
    938 
    939         // update whatever ProjectState is already present with new IAndroidTarget objects.
    940         synchronized (LOCK) {
    941             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    942                 loadTargetAndBuildTools(entry.getValue());
    943             }
    944         }
    945     }
    946 
    947     /**
    948      *  Cleans and unloads the SDK.
    949      */
    950     private void dispose() {
    951         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    952         monitor.removeProjectListener(mProjectListener);
    953         monitor.removeFileListener(mFileListener);
    954         monitor.removeResourceEventListener(mResourceEventListener);
    955 
    956         // the IAndroidTarget objects are now obsolete so update the project states.
    957         synchronized (LOCK) {
    958             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    959                 entry.getValue().setTarget(null);
    960             }
    961 
    962             // dispose of the target data.
    963             for (AndroidTargetData data : mTargetDataMap.values()) {
    964                 data.dispose();
    965             }
    966 
    967             mTargetDataMap.clear();
    968         }
    969     }
    970 
    971     void setTargetData(IAndroidTarget target, AndroidTargetData data) {
    972         synchronized (LOCK) {
    973             mTargetDataMap.put(target, data);
    974         }
    975     }
    976 
    977     /**
    978      * Returns the URL to the local documentation.
    979      * Can return null if no documentation is found in the current SDK.
    980      *
    981      * @param osDocsPath Path to the documentation folder in the current SDK.
    982      *  The folder may not actually exist.
    983      * @return A file:// URL on the local documentation folder if it exists or null.
    984      */
    985     private String getDocumentationBaseUrl(String osDocsPath) {
    986         File f = new File(osDocsPath);
    987 
    988         if (f.isDirectory()) {
    989             try {
    990                 // Note: to create a file:// URL, one would typically use something like
    991                 // f.toURI().toURL().toString(). However this generates a broken path on
    992                 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
    993                 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
    994                 // do the correct thing manually.
    995 
    996                 String path = f.getAbsolutePath();
    997                 if (File.separatorChar != '/') {
    998                     path = path.replace(File.separatorChar, '/');
    999                 }
   1000 
   1001                 // For some reason the URL class doesn't add the mandatory "//" after
   1002                 // the "file:" protocol name, so it has to be hacked into the path.
   1003                 URL url = new URL("file", null, "//" + path);  //$NON-NLS-1$ //$NON-NLS-2$
   1004                 String result = url.toString();
   1005                 return result;
   1006             } catch (MalformedURLException e) {
   1007                 // ignore malformed URLs
   1008             }
   1009         }
   1010 
   1011         return null;
   1012     }
   1013 
   1014     /**
   1015      * Delegate listener for project changes.
   1016      */
   1017     private IProjectListener mProjectListener = new IProjectListener() {
   1018         @Override
   1019         public void projectClosed(IProject project) {
   1020             onProjectRemoved(project, false /*deleted*/);
   1021         }
   1022 
   1023         @Override
   1024         public void projectDeleted(IProject project) {
   1025             onProjectRemoved(project, true /*deleted*/);
   1026         }
   1027 
   1028         private void onProjectRemoved(IProject removedProject, boolean deleted) {
   1029             if (DEBUG) {
   1030                 System.out.println(">>> CLOSED: " + removedProject.getName());
   1031             }
   1032 
   1033             // get the target project
   1034             synchronized (LOCK) {
   1035                 // Don't use getProject() as it could create the ProjectState if it's not
   1036                 // there yet and this is not what we want. We want the current object.
   1037                 // Therefore, direct access to the map.
   1038                 ProjectState removedState = sProjectStateMap.get(removedProject);
   1039                 if (removedState != null) {
   1040                     // 1. clear the layout lib cache associated with this project
   1041                     IAndroidTarget target = removedState.getTarget();
   1042                     if (target != null) {
   1043                         // get the bridge for the target, and clear the cache for this project.
   1044                         AndroidTargetData data = mTargetDataMap.get(target);
   1045                         if (data != null) {
   1046                             LayoutLibrary layoutLib = data.getLayoutLibrary();
   1047                             if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
   1048                                 layoutLib.clearCaches(removedProject);
   1049                             }
   1050                         }
   1051                     }
   1052 
   1053                     // 2. if the project is a library, make sure to update the
   1054                     // LibraryState for any project referencing it.
   1055                     // Also, record the updated projects that are libraries, to update
   1056                     // projects that depend on them.
   1057                     for (ProjectState projectState : sProjectStateMap.values()) {
   1058                         LibraryState libState = projectState.getLibrary(removedProject);
   1059                         if (libState != null) {
   1060                             // Close the library right away.
   1061                             // This remove links between the LibraryState and the projectState.
   1062                             // This is because in case of a rename of a project, projectClosed and
   1063                             // projectOpened will be called before any other job is run, so we
   1064                             // need to make sure projectOpened is closed with the main project
   1065                             // state up to date.
   1066                             libState.close();
   1067 
   1068                             // record that this project changed, and in case it's a library
   1069                             // that its parents need to be updated as well.
   1070                             markProject(projectState, projectState.isLibrary());
   1071                         }
   1072                     }
   1073 
   1074                     // now remove the project for the project map.
   1075                     sProjectStateMap.remove(removedProject);
   1076                 }
   1077             }
   1078 
   1079             if (DEBUG) {
   1080                 System.out.println("<<<");
   1081             }
   1082         }
   1083 
   1084         @Override
   1085         public void projectOpened(IProject project) {
   1086             onProjectOpened(project);
   1087         }
   1088 
   1089         @Override
   1090         public void projectOpenedWithWorkspace(IProject project) {
   1091             // no need to force recompilation when projects are opened with the workspace.
   1092             onProjectOpened(project);
   1093         }
   1094 
   1095         @Override
   1096         public void allProjectsOpenedWithWorkspace() {
   1097             // Correct currently open editors
   1098             fixOpenLegacyEditors();
   1099         }
   1100 
   1101         private void onProjectOpened(final IProject openedProject) {
   1102 
   1103             ProjectState openedState = getProjectState(openedProject);
   1104             if (openedState != null) {
   1105                 if (DEBUG) {
   1106                     System.out.println(">>> OPENED: " + openedProject.getName());
   1107                 }
   1108 
   1109                 synchronized (LOCK) {
   1110                     final boolean isLibrary = openedState.isLibrary();
   1111                     final boolean hasLibraries = openedState.hasLibraries();
   1112 
   1113                     if (isLibrary || hasLibraries) {
   1114                         boolean foundLibraries = false;
   1115                         // loop on all the existing project and update them based on this new
   1116                         // project
   1117                         for (ProjectState projectState : sProjectStateMap.values()) {
   1118                             if (projectState != openedState) {
   1119                                 // If the project has libraries, check if this project
   1120                                 // is a reference.
   1121                                 if (hasLibraries) {
   1122                                     // ProjectState#needs() both checks if this is a missing library
   1123                                     // and updates LibraryState to contains the new values.
   1124                                     // This must always be called.
   1125                                     LibraryState libState = openedState.needs(projectState);
   1126 
   1127                                     if (libState != null) {
   1128                                         // found a library! Add the main project to the list of
   1129                                         // modified project
   1130                                         foundLibraries = true;
   1131                                     }
   1132                                 }
   1133 
   1134                                 // if the project is a library check if the other project depend
   1135                                 // on it.
   1136                                 if (isLibrary) {
   1137                                     // ProjectState#needs() both checks if this is a missing library
   1138                                     // and updates LibraryState to contains the new values.
   1139                                     // This must always be called.
   1140                                     LibraryState libState = projectState.needs(openedState);
   1141 
   1142                                     if (libState != null) {
   1143                                         // There's a dependency! Add the project to the list of
   1144                                         // modified project, but also to a list of projects
   1145                                         // that saw one of its dependencies resolved.
   1146                                         markProject(projectState, projectState.isLibrary());
   1147                                     }
   1148                                 }
   1149                             }
   1150                         }
   1151 
   1152                         // if the project has a libraries and we found at least one, we add
   1153                         // the project to the list of modified project.
   1154                         // Since we already went through the parent, no need to update them.
   1155                         if (foundLibraries) {
   1156                             markProject(openedState, false /*updateParents*/);
   1157                         }
   1158                     }
   1159                 }
   1160 
   1161                 // Correct file editor associations.
   1162                 fixEditorAssociations(openedProject);
   1163 
   1164                 // Fix classpath entries in a job since the workspace might be locked now.
   1165                 Job fixCpeJob = new Job("Adjusting Android Project Classpath") {
   1166                     @Override
   1167                     protected IStatus run(IProgressMonitor monitor) {
   1168                         try {
   1169                             ProjectHelper.fixProjectClasspathEntries(
   1170                                     JavaCore.create(openedProject));
   1171                         } catch (JavaModelException e) {
   1172                             AdtPlugin.log(e, "error fixing classpath entries");
   1173                             // Don't return e2.getStatus(); the job control will then produce
   1174                             // a popup with this error, which isn't very interesting for the
   1175                             // user.
   1176                         }
   1177 
   1178                         return Status.OK_STATUS;
   1179                     }
   1180                 };
   1181 
   1182                 // build jobs are run after other interactive jobs
   1183                 fixCpeJob.setPriority(Job.BUILD);
   1184                 fixCpeJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
   1185                 fixCpeJob.schedule();
   1186 
   1187 
   1188                 if (DEBUG) {
   1189                     System.out.println("<<<");
   1190                 }
   1191             }
   1192         }
   1193 
   1194         @Override
   1195         public void projectRenamed(IProject project, IPath from) {
   1196             // we don't actually care about this anymore.
   1197         }
   1198     };
   1199 
   1200     /**
   1201      * Delegate listener for file changes.
   1202      */
   1203     private IFileListener mFileListener = new IFileListener() {
   1204         @Override
   1205         public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
   1206                 int kind, @Nullable String extension, int flags, boolean isAndroidPRoject) {
   1207             if (!isAndroidPRoject) {
   1208                 return;
   1209             }
   1210 
   1211             if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) &&
   1212                     file.getParent() == file.getProject()) {
   1213                 try {
   1214                     // reload the content of the project.properties file and update
   1215                     // the target.
   1216                     IProject iProject = file.getProject();
   1217 
   1218                     ProjectState state = Sdk.getProjectState(iProject);
   1219 
   1220                     // get the current target and build tools
   1221                     IAndroidTarget oldTarget = state.getTarget();
   1222                     boolean oldRsSupportMode = state.getRenderScriptSupportMode();
   1223 
   1224                     // get the current library flag
   1225                     boolean wasLibrary = state.isLibrary();
   1226 
   1227                     LibraryDifference diff = state.reloadProperties();
   1228 
   1229                     // load the (possibly new) target.
   1230                     IAndroidTarget newTarget = loadTargetAndBuildTools(state);
   1231 
   1232                     // reload the libraries if needed
   1233                     if (diff.hasDiff()) {
   1234                         if (diff.added) {
   1235                             synchronized (LOCK) {
   1236                                 for (ProjectState projectState : sProjectStateMap.values()) {
   1237                                     if (projectState != state) {
   1238                                         // need to call needs to do the libraryState link,
   1239                                         // but no need to look at the result, as we'll compare
   1240                                         // the result of getFullLibraryProjects()
   1241                                         // this is easier to due to indirect dependencies.
   1242                                         state.needs(projectState);
   1243                                     }
   1244                                 }
   1245                             }
   1246                         }
   1247 
   1248                         markProject(state, wasLibrary || state.isLibrary());
   1249                     }
   1250 
   1251                     // apply the new target if needed.
   1252                     if (newTarget != oldTarget ||
   1253                             oldRsSupportMode != state.getRenderScriptSupportMode()) {
   1254                         IJavaProject javaProject = BaseProjectHelper.getJavaProject(
   1255                                 file.getProject());
   1256                         if (javaProject != null) {
   1257                             ProjectHelper.updateProject(javaProject);
   1258                         }
   1259 
   1260                         // update the editors to reload with the new target
   1261                         AdtPlugin.getDefault().updateTargetListeners(iProject);
   1262                     }
   1263                 } catch (CoreException e) {
   1264                     // This can't happen as it's only for closed project (or non existing)
   1265                     // but in that case we can't get a fileChanged on this file.
   1266                 }
   1267             } else if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED) {
   1268                 // check if it's an add/remove on a jar files inside libs
   1269                 if (EXT_JAR.equals(extension) &&
   1270                         file.getProjectRelativePath().segmentCount() == 2 &&
   1271                         file.getParent().getName().equals(SdkConstants.FD_NATIVE_LIBS)) {
   1272                     // need to update the project and whatever depend on it.
   1273 
   1274                     processJarFileChange(file);
   1275                 }
   1276             }
   1277         }
   1278 
   1279         private void processJarFileChange(final IFile file) {
   1280             try {
   1281                 IProject iProject = file.getProject();
   1282 
   1283                 if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
   1284                     return;
   1285                 }
   1286 
   1287                 List<IJavaProject> projectList = new ArrayList<IJavaProject>();
   1288                 IJavaProject javaProject = BaseProjectHelper.getJavaProject(iProject);
   1289                 if (javaProject != null) {
   1290                     projectList.add(javaProject);
   1291                 }
   1292 
   1293                 ProjectState state = Sdk.getProjectState(iProject);
   1294 
   1295                 if (state != null) {
   1296                     Collection<ProjectState> parents = state.getFullParentProjects();
   1297                     for (ProjectState s : parents) {
   1298                         javaProject = BaseProjectHelper.getJavaProject(s.getProject());
   1299                         if (javaProject != null) {
   1300                             projectList.add(javaProject);
   1301                         }
   1302                     }
   1303 
   1304                     ProjectHelper.updateProjects(
   1305                             projectList.toArray(new IJavaProject[projectList.size()]));
   1306                 }
   1307             } catch (CoreException e) {
   1308                 // This can't happen as it's only for closed project (or non existing)
   1309                 // but in that case we can't get a fileChanged on this file.
   1310             }
   1311         }
   1312     };
   1313 
   1314     /** List of modified projects. This is filled in
   1315      * {@link IProjectListener#projectOpened(IProject)},
   1316      * {@link IProjectListener#projectOpenedWithWorkspace(IProject)},
   1317      * {@link IProjectListener#projectClosed(IProject)}, and
   1318      * {@link IProjectListener#projectDeleted(IProject)} and processed in
   1319      * {@link IResourceEventListener#resourceChangeEventEnd()}.
   1320      */
   1321     private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>();
   1322     private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>();
   1323 
   1324     private void markProject(ProjectState projectState, boolean updateParents) {
   1325         if (mModifiedProjects.contains(projectState) == false) {
   1326             if (DEBUG) {
   1327                 System.out.println("\tMARKED: " + projectState.getProject().getName());
   1328             }
   1329             mModifiedProjects.add(projectState);
   1330         }
   1331 
   1332         // if the project is resolved also add it to this list.
   1333         if (updateParents) {
   1334             if (mModifiedChildProjects.contains(projectState) == false) {
   1335                 if (DEBUG) {
   1336                     System.out.println("\tMARKED(child): " + projectState.getProject().getName());
   1337                 }
   1338                 mModifiedChildProjects.add(projectState);
   1339             }
   1340         }
   1341     }
   1342 
   1343     /**
   1344      * Delegate listener for resource changes. This is called before and after any calls to the
   1345      * project and file listeners (for a given resource change event).
   1346      */
   1347     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
   1348         @Override
   1349         public void resourceChangeEventStart() {
   1350             mModifiedProjects.clear();
   1351             mModifiedChildProjects.clear();
   1352         }
   1353 
   1354         @Override
   1355         public void resourceChangeEventEnd() {
   1356             if (mModifiedProjects.size() == 0) {
   1357                 return;
   1358             }
   1359 
   1360             // first make sure all the parents are updated
   1361             updateParentProjects();
   1362 
   1363             // for all modified projects, update their library list
   1364             // and gather their IProject
   1365             final List<IJavaProject> projectList = new ArrayList<IJavaProject>();
   1366             for (ProjectState state : mModifiedProjects) {
   1367                 state.updateFullLibraryList();
   1368                 projectList.add(JavaCore.create(state.getProject()));
   1369             }
   1370 
   1371             Job job = new Job("Android Library Update") { //$NON-NLS-1$
   1372                 @Override
   1373                 protected IStatus run(IProgressMonitor monitor) {
   1374                     LibraryClasspathContainerInitializer.updateProjects(
   1375                             projectList.toArray(new IJavaProject[projectList.size()]));
   1376 
   1377                     for (IJavaProject javaProject : projectList) {
   1378                         try {
   1379                             javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD,
   1380                                     monitor);
   1381                         } catch (CoreException e) {
   1382                             // pass
   1383                         }
   1384                     }
   1385                     return Status.OK_STATUS;
   1386                 }
   1387             };
   1388             job.setPriority(Job.BUILD);
   1389             job.setRule(ResourcesPlugin.getWorkspace().getRoot());
   1390             job.schedule();
   1391         }
   1392     };
   1393 
   1394     /**
   1395      * Updates all existing projects with a given list of new/updated libraries.
   1396      * This loops through all opened projects and check if they depend on any of the given
   1397      * library project, and if they do, they are linked together.
   1398      */
   1399     private void updateParentProjects() {
   1400         if (mModifiedChildProjects.size() == 0) {
   1401             return;
   1402         }
   1403 
   1404         ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects);
   1405         mModifiedChildProjects.clear();
   1406         synchronized (LOCK) {
   1407             // for each project for which we must update its parent, we loop on the parent
   1408             // projects and adds them to the list of modified projects. If they are themselves
   1409             // libraries, we add them too.
   1410             for (ProjectState state : childProjects) {
   1411                 if (DEBUG) {
   1412                     System.out.println(">>> Updating parents of " + state.getProject().getName());
   1413                 }
   1414                 List<ProjectState> parents = state.getParentProjects();
   1415                 for (ProjectState parent : parents) {
   1416                     markProject(parent, parent.isLibrary());
   1417                 }
   1418                 if (DEBUG) {
   1419                     System.out.println("<<<");
   1420                 }
   1421             }
   1422         }
   1423 
   1424         // done, but there may be parents that are also libraries. Need to update their parents.
   1425         updateParentProjects();
   1426     }
   1427 
   1428     /**
   1429      * Fix editor associations for the given project, if not already done.
   1430      * <p/>
   1431      * Eclipse has a per-file setting for which editor should be used for each file
   1432      * (see {@link IDE#setDefaultEditor(IFile, String)}).
   1433      * We're using this flag to pick between the various XML editors (layout, drawable, etc)
   1434      * since they all have the same file name extension.
   1435      * <p/>
   1436      * Unfortunately, the file setting can be "wrong" for two reasons:
   1437      * <ol>
   1438      *   <li> The editor type was added <b>after</b> a file had been seen by the IDE.
   1439      *        For example, we added new editors for animations and for drawables around
   1440      *        ADT 12, but any file seen by ADT in earlier versions will continue to use
   1441      *        the vanilla Eclipse XML editor instead.
   1442      *   <li> A bug in ADT 14 and ADT 15 (see issue 21124) meant that files created in new
   1443      *        folders would end up with wrong editor associations. Even though that bug
   1444      *        is fixed in ADT 16, the fix only affects new files, it cannot retroactively
   1445      *        fix editor associations that were set incorrectly by ADT 14 or 15.
   1446      * </ol>
   1447      * <p/>
   1448      * This method attempts to fix the editor bindings retroactively by scanning all the
   1449      * resource XML files and resetting the editor associations.
   1450      * Since this is a potentially slow operation, this is only done "once"; we use a
   1451      * persistent project property to avoid looking repeatedly. In the future if we add
   1452      * additional editors, we can rev the scanned version value.
   1453      */
   1454     private void fixEditorAssociations(final IProject project) {
   1455         QualifiedName KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "editorbinding"); //$NON-NLS-1$
   1456 
   1457         try {
   1458             String value = project.getPersistentProperty(KEY);
   1459             int currentVersion = 0;
   1460             if (value != null) {
   1461                 try {
   1462                     currentVersion = Integer.parseInt(value);
   1463                 } catch (Exception ingore) {
   1464                 }
   1465             }
   1466 
   1467             // The target version we're comparing to. This must be incremented each time
   1468             // we change the processing here so that a new version of the plugin would
   1469             // try to fix existing user projects.
   1470             final int targetVersion = 2;
   1471 
   1472             if (currentVersion >= targetVersion) {
   1473                 return;
   1474             }
   1475 
   1476             // Set to specific version such that we can rev the version in the future
   1477             // to trigger further scanning
   1478             project.setPersistentProperty(KEY, Integer.toString(targetVersion));
   1479 
   1480             // Now update the actual editor associations.
   1481             Job job = new Job("Update Android editor bindings") { //$NON-NLS-1$
   1482                 @Override
   1483                 protected IStatus run(IProgressMonitor monitor) {
   1484                     try {
   1485                         for (IResource folderResource : project.getFolder(FD_RES).members()) {
   1486                             if (folderResource instanceof IFolder) {
   1487                                 IFolder folder = (IFolder) folderResource;
   1488 
   1489                                 for (IResource resource : folder.members()) {
   1490                                     if (resource instanceof IFile &&
   1491                                             resource.getName().endsWith(DOT_XML)) {
   1492                                         fixXmlFile((IFile) resource);
   1493                                     }
   1494                                 }
   1495                             }
   1496                         }
   1497 
   1498                         // TODO change AndroidManifest.xml ID too
   1499 
   1500                     } catch (CoreException e) {
   1501                         AdtPlugin.log(e, null);
   1502                     }
   1503 
   1504                     return Status.OK_STATUS;
   1505                 }
   1506 
   1507                 /**
   1508                  * Attempt to fix the editor ID for the given /res XML file.
   1509                  */
   1510                 private void fixXmlFile(final IFile file) {
   1511                     // Fix the default editor ID for this resource.
   1512                     // This has no effect on currently open editors.
   1513                     IEditorDescriptor desc = IDE.getDefaultEditor(file);
   1514 
   1515                     if (desc == null || !CommonXmlEditor.ID.equals(desc.getId())) {
   1516                         IDE.setDefaultEditor(file, CommonXmlEditor.ID);
   1517                     }
   1518                 }
   1519             };
   1520             job.setPriority(Job.BUILD);
   1521             job.schedule();
   1522         } catch (CoreException e) {
   1523             AdtPlugin.log(e, null);
   1524         }
   1525     }
   1526 
   1527     /**
   1528      * Tries to fix all currently open Android legacy editors.
   1529      * <p/>
   1530      * If an editor is found to match one of the legacy ids, we'll try to close it.
   1531      * If that succeeds, we try to reopen it using the new common editor ID.
   1532      * <p/>
   1533      * This method must be run from the UI thread.
   1534      */
   1535     private void fixOpenLegacyEditors() {
   1536 
   1537         AdtPlugin adt = AdtPlugin.getDefault();
   1538         if (adt == null) {
   1539             return;
   1540         }
   1541 
   1542         final IPreferenceStore store = adt.getPreferenceStore();
   1543         int currentValue = store.getInt(AdtPrefs.PREFS_FIX_LEGACY_EDITORS);
   1544         // The target version we're comparing to. This must be incremented each time
   1545         // we change the processing here so that a new version of the plugin would
   1546         // try to fix existing editors.
   1547         final int targetValue = 1;
   1548 
   1549         if (currentValue >= targetValue) {
   1550             return;
   1551         }
   1552 
   1553         // To be able to close and open editors we need to make sure this is done
   1554         // in the UI thread, which this isn't invoked from.
   1555         PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
   1556             @Override
   1557             public void run() {
   1558                 HashSet<String> legacyIds =
   1559                     new HashSet<String>(Arrays.asList(CommonXmlEditor.LEGACY_EDITOR_IDS));
   1560 
   1561                 for (IWorkbenchWindow win : PlatformUI.getWorkbench().getWorkbenchWindows()) {
   1562                     for (IWorkbenchPage page : win.getPages()) {
   1563                         for (IEditorReference ref : page.getEditorReferences()) {
   1564                             try {
   1565                                 IEditorInput input = ref.getEditorInput();
   1566                                 if (input instanceof IFileEditorInput) {
   1567                                     IFile file = ((IFileEditorInput)input).getFile();
   1568                                     IEditorPart part = ref.getEditor(true /*restore*/);
   1569                                     if (part != null) {
   1570                                         IWorkbenchPartSite site = part.getSite();
   1571                                         if (site != null) {
   1572                                             String id = site.getId();
   1573                                             if (legacyIds.contains(id)) {
   1574                                                 // This editor matches one of legacy editor IDs.
   1575                                                 fixEditor(page, part, input, file, id);
   1576                                             }
   1577                                         }
   1578                                     }
   1579                                 }
   1580                             } catch (Exception e) {
   1581                                 // ignore
   1582                             }
   1583                         }
   1584                     }
   1585                 }
   1586 
   1587                 // Remember that we managed to do fix all editors
   1588                 store.setValue(AdtPrefs.PREFS_FIX_LEGACY_EDITORS, targetValue);
   1589             }
   1590 
   1591             private void fixEditor(
   1592                     IWorkbenchPage page,
   1593                     IEditorPart part,
   1594                     IEditorInput input,
   1595                     IFile file,
   1596                     String id) {
   1597                 IDE.setDefaultEditor(file, CommonXmlEditor.ID);
   1598 
   1599                 boolean ok = page.closeEditor(part, true /*save*/);
   1600 
   1601                 AdtPlugin.log(IStatus.INFO,
   1602                     "Closed legacy editor ID %s for %s: %s", //$NON-NLS-1$
   1603                     id,
   1604                     file.getFullPath(),
   1605                     ok ? "Success" : "Failed");//$NON-NLS-1$ //$NON-NLS-2$
   1606 
   1607                 if (ok) {
   1608                     // Try to reopen it with the new ID
   1609                     try {
   1610                         page.openEditor(input, CommonXmlEditor.ID);
   1611                     } catch (PartInitException e) {
   1612                         AdtPlugin.log(e,
   1613                             "Failed to reopen %s",          //$NON-NLS-1$
   1614                             file.getFullPath());
   1615                     }
   1616                 }
   1617             }
   1618         });
   1619     }
   1620 }
   1621