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