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 com.android.ddmlib.IDevice;
     20 import com.android.ide.common.rendering.LayoutLibrary;
     21 import com.android.ide.common.sdk.LoadStatus;
     22 import com.android.ide.eclipse.adt.AdtConstants;
     23 import com.android.ide.eclipse.adt.AdtPlugin;
     24 import com.android.ide.eclipse.adt.internal.build.DexWrapper;
     25 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer;
     26 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     27 import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
     28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
     29 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
     30 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
     31 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
     32 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
     33 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
     34 import com.android.io.StreamException;
     35 import com.android.prefs.AndroidLocation.AndroidLocationException;
     36 import com.android.sdklib.AndroidVersion;
     37 import com.android.sdklib.IAndroidTarget;
     38 import com.android.sdklib.ISdkLog;
     39 import com.android.sdklib.SdkConstants;
     40 import com.android.sdklib.SdkManager;
     41 import com.android.sdklib.internal.avd.AvdManager;
     42 import com.android.sdklib.internal.project.ProjectProperties;
     43 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
     44 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
     45 
     46 import org.eclipse.core.resources.IFile;
     47 import org.eclipse.core.resources.IMarkerDelta;
     48 import org.eclipse.core.resources.IProject;
     49 import org.eclipse.core.resources.IResourceDelta;
     50 import org.eclipse.core.resources.IncrementalProjectBuilder;
     51 import org.eclipse.core.runtime.CoreException;
     52 import org.eclipse.core.runtime.IPath;
     53 import org.eclipse.core.runtime.IProgressMonitor;
     54 import org.eclipse.core.runtime.IStatus;
     55 import org.eclipse.core.runtime.Status;
     56 import org.eclipse.core.runtime.jobs.Job;
     57 import org.eclipse.jdt.core.IJavaProject;
     58 import org.eclipse.jdt.core.JavaCore;
     59 
     60 import java.io.File;
     61 import java.io.IOException;
     62 import java.net.MalformedURLException;
     63 import java.net.URL;
     64 import java.util.ArrayList;
     65 import java.util.HashMap;
     66 import java.util.HashSet;
     67 import java.util.List;
     68 import java.util.Map;
     69 import java.util.Set;
     70 import java.util.Map.Entry;
     71 
     72 /**
     73  * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
     74  * at the same time.
     75  *
     76  * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
     77  * the Sdk object.
     78  *
     79  * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
     80  */
     81 public final class Sdk  {
     82     private final static boolean DEBUG = false;
     83 
     84     private final static Object LOCK = new Object();
     85 
     86     private static Sdk sCurrentSdk = null;
     87 
     88     /**
     89      * Map associating {@link IProject} and their state {@link ProjectState}.
     90      * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
     91      */
     92     private final static HashMap<IProject, ProjectState> sProjectStateMap =
     93             new HashMap<IProject, ProjectState>();
     94 
     95     /**
     96      * Data bundled using during the load of Target data.
     97      * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
     98      * to compile before the loading was finished. Those projects will be recompiled
     99      * at the end of the loading.
    100      */
    101     private final static class TargetLoadBundle {
    102         LoadStatus status;
    103         final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>();
    104     }
    105 
    106     private final SdkManager mManager;
    107     private final DexWrapper mDexWrapper;
    108     private final AvdManager mAvdManager;
    109 
    110     /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
    111     private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
    112         new HashMap<IAndroidTarget, AndroidTargetData>();
    113     /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
    114     private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
    115         new HashMap<IAndroidTarget, TargetLoadBundle>();
    116 
    117     /**
    118      * If true the target data will never load anymore. The only way to reload them is to
    119      * completely reload the SDK with {@link #loadSdk(String)}
    120      */
    121     private boolean mDontLoadTargetData = false;
    122 
    123     private final String mDocBaseUrl;
    124 
    125     private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager();
    126 
    127     /**
    128      * Classes implementing this interface will receive notification when targets are changed.
    129      */
    130     public interface ITargetChangeListener {
    131         /**
    132          * Sent when project has its target changed.
    133          */
    134         void onProjectTargetChange(IProject changedProject);
    135 
    136         /**
    137          * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
    138          * or the SDK is changed).
    139          */
    140         void onTargetLoaded(IAndroidTarget target);
    141 
    142         /**
    143          * Called when the base content of the SDK is parsed.
    144          */
    145         void onSdkLoaded();
    146     }
    147 
    148     /**
    149      * Basic abstract implementation of the ITargetChangeListener for the case where both
    150      * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
    151      * use the same code based on a simple test requiring to know the current IProject.
    152      */
    153     public static abstract class TargetChangeListener implements ITargetChangeListener {
    154         /**
    155          * Returns the {@link IProject} associated with the listener.
    156          */
    157         public abstract IProject getProject();
    158 
    159         /**
    160          * Called when the listener needs to take action on the event. This is only called
    161          * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
    162          * match the values received in {@link #onProjectTargetChange(IProject)} and
    163          * {@link #onTargetLoaded(IAndroidTarget)}.
    164          */
    165         public abstract void reload();
    166 
    167         public void onProjectTargetChange(IProject changedProject) {
    168             if (changedProject != null && changedProject.equals(getProject())) {
    169                 reload();
    170             }
    171         }
    172 
    173         public void onTargetLoaded(IAndroidTarget target) {
    174             IProject project = getProject();
    175             if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
    176                 reload();
    177             }
    178         }
    179 
    180         public void onSdkLoaded() {
    181             // do nothing;
    182         }
    183     }
    184 
    185     /**
    186      * Returns the lock object used to synchronize all operations dealing with SDK, targets and
    187      * projects.
    188      */
    189     public static final Object getLock() {
    190         return LOCK;
    191     }
    192 
    193     /**
    194      * Loads an SDK and returns an {@link Sdk} object if success.
    195      * <p/>If the SDK failed to load, it displays an error to the user.
    196      * @param sdkLocation the OS path to the SDK.
    197      */
    198     public static Sdk loadSdk(String sdkLocation) {
    199         synchronized (LOCK) {
    200             if (sCurrentSdk != null) {
    201                 sCurrentSdk.dispose();
    202                 sCurrentSdk = null;
    203             }
    204 
    205             final ArrayList<String> logMessages = new ArrayList<String>();
    206             ISdkLog log = new ISdkLog() {
    207                 public void error(Throwable throwable, String errorFormat, Object... arg) {
    208                     if (errorFormat != null) {
    209                         logMessages.add(String.format("Error: " + errorFormat, arg));
    210                     }
    211 
    212                     if (throwable != null) {
    213                         logMessages.add(throwable.getMessage());
    214                     }
    215                 }
    216 
    217                 public void warning(String warningFormat, Object... arg) {
    218                     logMessages.add(String.format("Warning: " + warningFormat, arg));
    219                 }
    220 
    221                 public void printf(String msgFormat, Object... arg) {
    222                     logMessages.add(String.format(msgFormat, arg));
    223                 }
    224             };
    225 
    226             // get an SdkManager object for the location
    227             SdkManager manager = SdkManager.createManager(sdkLocation, log);
    228             if (manager != null) {
    229                 // load DX.
    230                 DexWrapper dexWrapper = new DexWrapper();
    231                 String dexLocation =
    232                         sdkLocation + File.separator +
    233                         SdkConstants.OS_SDK_PLATFORM_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR;
    234                 IStatus res = dexWrapper.loadDex(dexLocation);
    235                 if (res != Status.OK_STATUS) {
    236                     log.error(null, res.getMessage());
    237                     dexWrapper = null;
    238                 }
    239 
    240                 // create the AVD Manager
    241                 AvdManager avdManager = null;
    242                 try {
    243                     avdManager = new AvdManager(manager, log);
    244                 } catch (AndroidLocationException e) {
    245                     log.error(e, "Error parsing the AVDs");
    246                 }
    247                 sCurrentSdk = new Sdk(manager, dexWrapper, avdManager);
    248                 return sCurrentSdk;
    249             } else {
    250                 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
    251                 for (String msg : logMessages) {
    252                     sb.append('\n');
    253                     sb.append(msg);
    254                 }
    255                 AdtPlugin.displayError("Android SDK", sb.toString());
    256             }
    257             return null;
    258         }
    259     }
    260 
    261     /**
    262      * Returns the current {@link Sdk} object.
    263      */
    264     public static Sdk getCurrent() {
    265         synchronized (LOCK) {
    266             return sCurrentSdk;
    267         }
    268     }
    269 
    270     /**
    271      * Returns the location (OS path) of the current SDK.
    272      */
    273     public String getSdkLocation() {
    274         return mManager.getLocation();
    275     }
    276 
    277     /**
    278      * Returns the URL to the local documentation.
    279      * Can return null if no documentation is found in the current SDK.
    280      *
    281      * @return A file:// URL on the local documentation folder if it exists or null.
    282      */
    283     public String getDocumentationBaseUrl() {
    284         return mDocBaseUrl;
    285     }
    286 
    287     /**
    288      * Returns the list of targets that are available in the SDK.
    289      */
    290     public IAndroidTarget[] getTargets() {
    291         return mManager.getTargets();
    292     }
    293 
    294     /**
    295      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
    296      *
    297      * @param hash the {@link IAndroidTarget} hash string.
    298      * @return The matching {@link IAndroidTarget} or null.
    299      */
    300     public IAndroidTarget getTargetFromHashString(String hash) {
    301         return mManager.getTargetFromHashString(hash);
    302     }
    303 
    304     /**
    305      * Initializes a new project with a target. This creates the <code>project.properties</code>
    306      * file.
    307      * @param project the project to intialize
    308      * @param target the project's target.
    309      * @throws IOException if creating the file failed in any way.
    310      * @throws StreamException
    311      */
    312     public void initProject(IProject project, IAndroidTarget target)
    313             throws IOException, StreamException {
    314         if (project == null || target == null) {
    315             return;
    316         }
    317 
    318         synchronized (LOCK) {
    319             // check if there's already a state?
    320             ProjectState state = getProjectState(project);
    321 
    322             ProjectPropertiesWorkingCopy properties = null;
    323 
    324             if (state != null) {
    325                 properties = state.getProperties().makeWorkingCopy();
    326             }
    327 
    328             if (properties == null) {
    329                 IPath location = project.getLocation();
    330                 if (location == null) {  // can return null when the project is being deleted.
    331                     // do nothing and return null;
    332                     return;
    333                 }
    334 
    335                 properties = ProjectProperties.create(location.toOSString(), PropertyType.PROJECT);
    336             }
    337 
    338             // save the target hash string in the project persistent property
    339             properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
    340             properties.save();
    341         }
    342     }
    343 
    344     /**
    345      * Returns the {@link ProjectState} object associated with a given project.
    346      * <p/>
    347      * This method is the only way to properly get the project's {@link ProjectState}
    348      * If the project has not yet been loaded, then it is loaded.
    349      * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
    350      * objects, and therefore is static.
    351      * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
    352      * are replaced.
    353      * @param project the request project
    354      * @return the ProjectState for the project.
    355      */
    356     @SuppressWarnings("deprecation")
    357     public static ProjectState getProjectState(IProject project) {
    358         if (project == null) {
    359             return null;
    360         }
    361 
    362         synchronized (LOCK) {
    363             ProjectState state = sProjectStateMap.get(project);
    364             if (state == null) {
    365                 // load the project.properties from the project folder.
    366                 IPath location = project.getLocation();
    367                 if (location == null) {  // can return null when the project is being deleted.
    368                     // do nothing and return null;
    369                     return null;
    370                 }
    371 
    372                 String projectLocation = location.toOSString();
    373 
    374                 ProjectProperties properties = ProjectProperties.load(projectLocation,
    375                         PropertyType.PROJECT);
    376                 if (properties == null) {
    377                     // legacy support: look for default.properties and rename it if needed.
    378                     properties = ProjectProperties.load(projectLocation,
    379                             PropertyType.LEGACY_DEFAULT);
    380 
    381                     if (properties == null) {
    382                         AdtPlugin.log(IStatus.ERROR,
    383                                 "Failed to load properties file for project '%s'",
    384                                 project.getName());
    385                         return null;
    386                     } else {
    387                         //legacy mode.
    388                         // get a working copy with the new type "project"
    389                         ProjectPropertiesWorkingCopy wc = properties.makeWorkingCopy(
    390                                 PropertyType.PROJECT);
    391                         // and save it
    392                         try {
    393                             wc.save();
    394 
    395                             // delete the old file.
    396                             ProjectProperties.delete(projectLocation, PropertyType.LEGACY_DEFAULT);
    397                         } catch (Exception e) {
    398                             AdtPlugin.log(IStatus.ERROR,
    399                                     "Failed to rename properties file to %1$s for project '%s2$'",
    400                                     PropertyType.PROJECT.getFilename(), project.getName());
    401                         }
    402                     }
    403                 }
    404 
    405                 state = new ProjectState(project, properties);
    406                 sProjectStateMap.put(project, state);
    407 
    408                 // try to resolve the target
    409                 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
    410                     sCurrentSdk.loadTarget(state);
    411                 }
    412             }
    413 
    414             return state;
    415         }
    416     }
    417 
    418     /**
    419      * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
    420      */
    421     public IAndroidTarget getTarget(IProject project) {
    422         if (project == null) {
    423             return null;
    424         }
    425 
    426         ProjectState state = getProjectState(project);
    427         if (state != null) {
    428             return state.getTarget();
    429         }
    430 
    431         return null;
    432     }
    433 
    434     /**
    435      * Loads the {@link IAndroidTarget} for a given project.
    436      * <p/>This method will get the target hash string from the project properties, and resolve
    437      * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
    438      * @param state the state representing the project to load.
    439      * @return the target that was loaded.
    440      */
    441     public IAndroidTarget loadTarget(ProjectState state) {
    442         IAndroidTarget target = null;
    443         if (state != null) {
    444             String hash = state.getTargetHashString();
    445             if (hash != null) {
    446                 state.setTarget(target = getTargetFromHashString(hash));
    447             }
    448         }
    449 
    450         return target;
    451     }
    452 
    453     /**
    454      * Checks and loads (if needed) the data for a given target.
    455      * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
    456      * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
    457      * <p/>An optional project as second parameter can be given to be recompiled once the target
    458      * data is finished loading.
    459      * <p/>The return value is non-null only if the target data has already been loaded (and in this
    460      * case is the status of the load operation)
    461      * @param target the target to load.
    462      * @param project an optional project to be recompiled when the target data is loaded.
    463      * If the target is already loaded, nothing happens.
    464      * @return The load status if the target data is already loaded.
    465      */
    466     public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
    467         boolean loadData = false;
    468 
    469         synchronized (LOCK) {
    470             if (mDontLoadTargetData) {
    471                 return LoadStatus.FAILED;
    472             }
    473 
    474             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    475             if (bundle == null) {
    476                 bundle = new TargetLoadBundle();
    477                 mTargetDataStatusMap.put(target,bundle);
    478 
    479                 // set status to loading
    480                 bundle.status = LoadStatus.LOADING;
    481 
    482                 // add project to bundle
    483                 if (project != null) {
    484                     bundle.projecsToReload.add(project);
    485                 }
    486 
    487                 // and set the flag to start the loading below
    488                 loadData = true;
    489             } else if (bundle.status == LoadStatus.LOADING) {
    490                 // add project to bundle
    491                 if (project != null) {
    492                     bundle.projecsToReload.add(project);
    493                 }
    494 
    495                 return bundle.status;
    496             } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
    497                 return bundle.status;
    498             }
    499         }
    500 
    501         if (loadData) {
    502             Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
    503                 @Override
    504                 protected IStatus run(IProgressMonitor monitor) {
    505                     AdtPlugin plugin = AdtPlugin.getDefault();
    506                     try {
    507                         IStatus status = new AndroidTargetParser(target).run(monitor);
    508 
    509                         IJavaProject[] javaProjectArray = null;
    510 
    511                         synchronized (LOCK) {
    512                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    513 
    514                             if (status.getCode() != IStatus.OK) {
    515                                 bundle.status = LoadStatus.FAILED;
    516                                 bundle.projecsToReload.clear();
    517                             } else {
    518                                 bundle.status = LoadStatus.LOADED;
    519 
    520                                 // Prepare the array of project to recompile.
    521                                 // The call is done outside of the synchronized block.
    522                                 javaProjectArray = bundle.projecsToReload.toArray(
    523                                         new IJavaProject[bundle.projecsToReload.size()]);
    524 
    525                                 // and update the UI of the editors that depend on the target data.
    526                                 plugin.updateTargetListeners(target);
    527                             }
    528                         }
    529 
    530                         if (javaProjectArray != null) {
    531                             AndroidClasspathContainerInitializer.updateProjects(javaProjectArray);
    532                         }
    533 
    534                         return status;
    535                     } catch (Throwable t) {
    536                         synchronized (LOCK) {
    537                             TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
    538                             bundle.status = LoadStatus.FAILED;
    539                         }
    540 
    541                         AdtPlugin.log(t, "Exception in checkAndLoadTargetData.");    //$NON-NLS-1$
    542                         return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    543                                 String.format(
    544                                         "Parsing Data for %1$s failed", //$NON-NLS-1$
    545                                         target.hashString()),
    546                                 t);
    547                     }
    548                 }
    549             };
    550             job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
    551             job.schedule();
    552         }
    553 
    554         // The only way to go through here is when the loading starts through the Job.
    555         // Therefore the current status of the target is LOADING.
    556         return LoadStatus.LOADING;
    557     }
    558 
    559     /**
    560      * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
    561      */
    562     public AndroidTargetData getTargetData(IAndroidTarget target) {
    563         synchronized (LOCK) {
    564             return mTargetDataMap.get(target);
    565         }
    566     }
    567 
    568     /**
    569      * Return the {@link AndroidTargetData} for a given {@link IProject}.
    570      */
    571     public AndroidTargetData getTargetData(IProject project) {
    572         synchronized (LOCK) {
    573             IAndroidTarget target = getTarget(project);
    574             if (target != null) {
    575                 return getTargetData(target);
    576             }
    577         }
    578 
    579         return null;
    580     }
    581 
    582     /**
    583      * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
    584      * loaded properly, then this will return <code>null</code>.
    585      */
    586     public DexWrapper getDexWrapper() {
    587         return mDexWrapper;
    588     }
    589 
    590     /**
    591      * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
    592      * be <code>null</code>.
    593      */
    594     public AvdManager getAvdManager() {
    595         return mAvdManager;
    596     }
    597 
    598     public static AndroidVersion getDeviceVersion(IDevice device) {
    599         try {
    600             Map<String, String> props = device.getProperties();
    601             String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
    602             if (apiLevel == null) {
    603                 return null;
    604             }
    605 
    606             return new AndroidVersion(Integer.parseInt(apiLevel),
    607                     props.get((IDevice.PROP_BUILD_CODENAME)));
    608         } catch (NumberFormatException e) {
    609             return null;
    610         }
    611     }
    612 
    613     public LayoutDeviceManager getLayoutDeviceManager() {
    614         return mLayoutDeviceManager;
    615     }
    616 
    617     /**
    618      * Returns a list of {@link ProjectState} representing projects depending, directly or
    619      * indirectly on a given library project.
    620      * @param project the library project.
    621      * @return a possibly empty list of ProjectState.
    622      */
    623     public static Set<ProjectState> getMainProjectsFor(IProject project) {
    624         synchronized (LOCK) {
    625             // first get the project directly depending on this.
    626             HashSet<ProjectState> list = new HashSet<ProjectState>();
    627 
    628             // loop on all project and see if ProjectState.getLibrary returns a non null
    629             // project.
    630             for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
    631                 if (project != entry.getKey()) {
    632                     LibraryState library = entry.getValue().getLibrary(project);
    633                     if (library != null) {
    634                         list.add(entry.getValue());
    635                     }
    636                 }
    637             }
    638 
    639             // now look for projects depending on the projects directly depending on the library.
    640             HashSet<ProjectState> result = new HashSet<ProjectState>(list);
    641             for (ProjectState p : list) {
    642                 if (p.isLibrary()) {
    643                     Set<ProjectState> set = getMainProjectsFor(p.getProject());
    644                     result.addAll(set);
    645                 }
    646             }
    647 
    648             return result;
    649         }
    650     }
    651 
    652     /**
    653      * Unload the SDK's target data.
    654      *
    655      * If <var>preventReload</var>, this effect is final until the SDK instance is changed
    656      * through {@link #loadSdk(String)}.
    657      *
    658      * The goal is to unload the targets to be able to replace existing targets with new ones,
    659      * before calling {@link #loadSdk(String)} to fully reload the SDK.
    660      *
    661      * @param preventReload prevent the data from being loaded again for the remaining live of
    662      *   this {@link Sdk} instance.
    663      */
    664     public void unloadTargetData(boolean preventReload) {
    665         synchronized (LOCK) {
    666             mDontLoadTargetData = preventReload;
    667 
    668             // dispose of the target data.
    669             for (AndroidTargetData data : mTargetDataMap.values()) {
    670                 data.dispose();
    671             }
    672 
    673             mTargetDataMap.clear();
    674         }
    675     }
    676 
    677     private Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager) {
    678         mManager = manager;
    679         mDexWrapper = dexWrapper;
    680         mAvdManager = avdManager;
    681 
    682         // listen to projects closing
    683         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    684         // need to register the resource event listener first because the project listener
    685         // is called back during registration with project opened in the workspace.
    686         monitor.addResourceEventListener(mResourceEventListener);
    687         monitor.addProjectListener(mProjectListener);
    688         monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED);
    689 
    690         // pre-compute some paths
    691         mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
    692                 SdkConstants.OS_SDK_DOCS_FOLDER);
    693 
    694         // load the built-in and user layout devices
    695         mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation());
    696         // and the ones from the add-on
    697         loadLayoutDevices();
    698 
    699         // update whatever ProjectState is already present with new IAndroidTarget objects.
    700         synchronized (LOCK) {
    701             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    702                 entry.getValue().setTarget(
    703                         getTargetFromHashString(entry.getValue().getTargetHashString()));
    704             }
    705         }
    706     }
    707 
    708     /**
    709      *  Cleans and unloads the SDK.
    710      */
    711     private void dispose() {
    712         GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
    713         monitor.removeProjectListener(mProjectListener);
    714         monitor.removeFileListener(mFileListener);
    715         monitor.removeResourceEventListener(mResourceEventListener);
    716 
    717         // the IAndroidTarget objects are now obsolete so update the project states.
    718         synchronized (LOCK) {
    719             for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
    720                 entry.getValue().setTarget(null);
    721             }
    722 
    723             // dispose of the target data.
    724             for (AndroidTargetData data : mTargetDataMap.values()) {
    725                 data.dispose();
    726             }
    727 
    728             mTargetDataMap.clear();
    729         }
    730     }
    731 
    732     void setTargetData(IAndroidTarget target, AndroidTargetData data) {
    733         synchronized (LOCK) {
    734             mTargetDataMap.put(target, data);
    735         }
    736     }
    737 
    738     /**
    739      * Returns the URL to the local documentation.
    740      * Can return null if no documentation is found in the current SDK.
    741      *
    742      * @param osDocsPath Path to the documentation folder in the current SDK.
    743      *  The folder may not actually exist.
    744      * @return A file:// URL on the local documentation folder if it exists or null.
    745      */
    746     private String getDocumentationBaseUrl(String osDocsPath) {
    747         File f = new File(osDocsPath);
    748 
    749         if (f.isDirectory()) {
    750             try {
    751                 // Note: to create a file:// URL, one would typically use something like
    752                 // f.toURI().toURL().toString(). However this generates a broken path on
    753                 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
    754                 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
    755                 // do the correct thing manually.
    756 
    757                 String path = f.getAbsolutePath();
    758                 if (File.separatorChar != '/') {
    759                     path = path.replace(File.separatorChar, '/');
    760                 }
    761 
    762                 // For some reason the URL class doesn't add the mandatory "//" after
    763                 // the "file:" protocol name, so it has to be hacked into the path.
    764                 URL url = new URL("file", null, "//" + path);  //$NON-NLS-1$ //$NON-NLS-2$
    765                 String result = url.toString();
    766                 return result;
    767             } catch (MalformedURLException e) {
    768                 // ignore malformed URLs
    769             }
    770         }
    771 
    772         return null;
    773     }
    774 
    775     /**
    776      * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to
    777      * load {@link LayoutDevice} from them.
    778      */
    779     private void loadLayoutDevices() {
    780         IAndroidTarget[] targets = mManager.getTargets();
    781         for (IAndroidTarget target : targets) {
    782             if (target.isPlatform() == false) {
    783                 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML);
    784                 if (deviceXml.isFile()) {
    785                     mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml);
    786                 }
    787             }
    788         }
    789 
    790         mLayoutDeviceManager.sealAddonLayoutDevices();
    791     }
    792 
    793     /**
    794      * Delegate listener for project changes.
    795      */
    796     private IProjectListener mProjectListener = new IProjectListener() {
    797         public void projectClosed(IProject project) {
    798             onProjectRemoved(project, false /*deleted*/);
    799         }
    800 
    801         public void projectDeleted(IProject project) {
    802             onProjectRemoved(project, true /*deleted*/);
    803         }
    804 
    805         private void onProjectRemoved(IProject removedProject, boolean deleted) {
    806             try {
    807                 if (removedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
    808                     return;
    809                 }
    810             } catch (CoreException e) {
    811                 // this can only happen if the project does not exist or is not open, neither
    812                 // of which can happen here since we're processing a Project removed/deleted event
    813                 // which is processed before the project is actually removed/closed.
    814             }
    815 
    816             if (DEBUG) {
    817                 System.out.println(">>> CLOSED: " + removedProject.getName());
    818             }
    819 
    820             // get the target project
    821             synchronized (LOCK) {
    822                 // Don't use getProject() as it could create the ProjectState if it's not
    823                 // there yet and this is not what we want. We want the current object.
    824                 // Therefore, direct access to the map.
    825                 ProjectState removedState = sProjectStateMap.get(removedProject);
    826                 if (removedState != null) {
    827                     // 1. clear the layout lib cache associated with this project
    828                     IAndroidTarget target = removedState.getTarget();
    829                     if (target != null) {
    830                         // get the bridge for the target, and clear the cache for this project.
    831                         AndroidTargetData data = mTargetDataMap.get(target);
    832                         if (data != null) {
    833                             LayoutLibrary layoutLib = data.getLayoutLibrary();
    834                             if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
    835                                 layoutLib.clearCaches(removedProject);
    836                             }
    837                         }
    838                     }
    839 
    840                     // 2. if the project is a library, make sure to update the
    841                     // LibraryState for any project referencing it.
    842                     // Also, record the updated projects that are libraries, to update
    843                     // projects that depend on them.
    844                     for (ProjectState projectState : sProjectStateMap.values()) {
    845                         LibraryState libState = projectState.getLibrary(removedProject);
    846                         if (libState != null) {
    847                             // Close the library right away.
    848                             // This remove links between the LibraryState and the projectState.
    849                             // This is because in case of a rename of a project, projectClosed and
    850                             // projectOpened will be called before any other job is run, so we
    851                             // need to make sure projectOpened is closed with the main project
    852                             // state up to date.
    853                             libState.close();
    854 
    855                             // record that this project changed, and in case it's a library
    856                             // that its parents need to be updated as well.
    857                             markProject(projectState, projectState.isLibrary());
    858                         }
    859                     }
    860 
    861                     // now remove the project for the project map.
    862                     sProjectStateMap.remove(removedProject);
    863                 }
    864             }
    865 
    866             if (DEBUG) {
    867                 System.out.println("<<<");
    868             }
    869         }
    870 
    871         public void projectOpened(IProject project) {
    872             onProjectOpened(project);
    873         }
    874 
    875         public void projectOpenedWithWorkspace(IProject project) {
    876             // no need to force recompilation when projects are opened with the workspace.
    877             onProjectOpened(project);
    878         }
    879 
    880         private void onProjectOpened(final IProject openedProject) {
    881             try {
    882                 if (openedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
    883                     return;
    884                 }
    885             } catch (CoreException e) {
    886                 // this can only happen if the project does not exist or is not open, neither
    887                 // of which can happen here since we're processing a Project opened event.
    888             }
    889 
    890 
    891             ProjectState openedState = getProjectState(openedProject);
    892             if (openedState != null) {
    893                 if (DEBUG) {
    894                     System.out.println(">>> OPENED: " + openedProject.getName());
    895                 }
    896 
    897                 synchronized (LOCK) {
    898                     final boolean isLibrary = openedState.isLibrary();
    899                     final boolean hasLibraries = openedState.hasLibraries();
    900 
    901                     if (isLibrary || hasLibraries) {
    902                         boolean foundLibraries = false;
    903                         // loop on all the existing project and update them based on this new
    904                         // project
    905                         for (ProjectState projectState : sProjectStateMap.values()) {
    906                             if (projectState != openedState) {
    907                                 // If the project has libraries, check if this project
    908                                 // is a reference.
    909                                 if (hasLibraries) {
    910                                     // ProjectState#needs() both checks if this is a missing library
    911                                     // and updates LibraryState to contains the new values.
    912                                     // This must always be called.
    913                                     LibraryState libState = openedState.needs(projectState);
    914 
    915                                     if (libState != null) {
    916                                         // found a library! Add the main project to the list of
    917                                         // modified project
    918                                         foundLibraries = true;
    919                                     }
    920                                 }
    921 
    922                                 // if the project is a library check if the other project depend
    923                                 // on it.
    924                                 if (isLibrary) {
    925                                     // ProjectState#needs() both checks if this is a missing library
    926                                     // and updates LibraryState to contains the new values.
    927                                     // This must always be called.
    928                                     LibraryState libState = projectState.needs(openedState);
    929 
    930                                     if (libState != null) {
    931                                         // There's a dependency! Add the project to the list of
    932                                         // modified project, but also to a list of projects
    933                                         // that saw one of its dependencies resolved.
    934                                         markProject(projectState, projectState.isLibrary());
    935                                     }
    936                                 }
    937                             }
    938                         }
    939 
    940                         // if the project has a libraries and we found at least one, we add
    941                         // the project to the list of modified project.
    942                         // Since we already went through the parent, no need to update them.
    943                         if (foundLibraries) {
    944                             markProject(openedState, false /*updateParents*/);
    945                         }
    946                     }
    947                 }
    948 
    949                 if (DEBUG) {
    950                     System.out.println("<<<");
    951                 }
    952             }
    953         }
    954 
    955         public void projectRenamed(IProject project, IPath from) {
    956             // we don't actually care about this anymore.
    957         }
    958     };
    959 
    960     /**
    961      * Delegate listener for file changes.
    962      */
    963     private IFileListener mFileListener = new IFileListener() {
    964         public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) {
    965             if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) &&
    966                     file.getParent() == file.getProject()) {
    967                 try {
    968                     // reload the content of the project.properties file and update
    969                     // the target.
    970                     IProject iProject = file.getProject();
    971 
    972                     if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
    973                         return;
    974                     }
    975 
    976                     ProjectState state = Sdk.getProjectState(iProject);
    977 
    978                     // get the current target
    979                     IAndroidTarget oldTarget = state.getTarget();
    980 
    981                     // get the current library flag
    982                     boolean wasLibrary = state.isLibrary();
    983 
    984                     LibraryDifference diff = state.reloadProperties();
    985 
    986                     // load the (possibly new) target.
    987                     IAndroidTarget newTarget = loadTarget(state);
    988 
    989                     // reload the libraries if needed
    990                     if (diff.hasDiff()) {
    991                         if (diff.added) {
    992                             synchronized (LOCK) {
    993                                 for (ProjectState projectState : sProjectStateMap.values()) {
    994                                     if (projectState != state) {
    995                                         // need to call needs to do the libraryState link,
    996                                         // but no need to look at the result, as we'll compare
    997                                         // the result of getFullLibraryProjects()
    998                                         // this is easier to due to indirect dependencies.
    999                                         state.needs(projectState);
   1000                                     }
   1001                                 }
   1002                             }
   1003                         }
   1004 
   1005                         markProject(state, wasLibrary || state.isLibrary());
   1006                     }
   1007 
   1008                     // apply the new target if needed.
   1009                     if (newTarget != oldTarget) {
   1010                         IJavaProject javaProject = BaseProjectHelper.getJavaProject(
   1011                                 file.getProject());
   1012                         if (javaProject != null) {
   1013                             AndroidClasspathContainerInitializer.updateProjects(
   1014                                     new IJavaProject[] { javaProject });
   1015                         }
   1016 
   1017                         // update the editors to reload with the new target
   1018                         AdtPlugin.getDefault().updateTargetListeners(iProject);
   1019                     }
   1020                 } catch (CoreException e) {
   1021                     // This can't happen as it's only for closed project (or non existing)
   1022                     // but in that case we can't get a fileChanged on this file.
   1023                 }
   1024             }
   1025         }
   1026     };
   1027 
   1028     /** List of modified projects. This is filled in
   1029      * {@link IProjectListener#projectOpened(IProject)},
   1030      * {@link IProjectListener#projectOpenedWithWorkspace(IProject)},
   1031      * {@link IProjectListener#projectClosed(IProject)}, and
   1032      * {@link IProjectListener#projectDeleted(IProject)} and processed in
   1033      * {@link IResourceEventListener#resourceChangeEventEnd()}.
   1034      */
   1035     private final List<ProjectState> mModifiedProjects = new ArrayList<ProjectState>();
   1036     private final List<ProjectState> mModifiedChildProjects = new ArrayList<ProjectState>();
   1037 
   1038     private void markProject(ProjectState projectState, boolean updateParents) {
   1039         if (mModifiedProjects.contains(projectState) == false) {
   1040             if (DEBUG) {
   1041                 System.out.println("\tMARKED: " + projectState.getProject().getName());
   1042             }
   1043             mModifiedProjects.add(projectState);
   1044         }
   1045 
   1046         // if the project is resolved also add it to this list.
   1047         if (updateParents) {
   1048             if (mModifiedChildProjects.contains(projectState) == false) {
   1049                 if (DEBUG) {
   1050                     System.out.println("\tMARKED(child): " + projectState.getProject().getName());
   1051                 }
   1052                 mModifiedChildProjects.add(projectState);
   1053             }
   1054         }
   1055     }
   1056 
   1057     /**
   1058      * Delegate listener for resource changes. This is called before and after any calls to the
   1059      * project and file listeners (for a given resource change event).
   1060      */
   1061     private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
   1062         public void resourceChangeEventStart() {
   1063             mModifiedProjects.clear();
   1064             mModifiedChildProjects.clear();
   1065         }
   1066 
   1067         public void resourceChangeEventEnd() {
   1068             if (mModifiedProjects.size() == 0) {
   1069                 return;
   1070             }
   1071 
   1072             // first make sure all the parents are updated
   1073             updateParentProjects();
   1074 
   1075             // for all modified projects, update their library list
   1076             // and gather their IProject
   1077             final List<IJavaProject> projectList = new ArrayList<IJavaProject>();
   1078             for (ProjectState state : mModifiedProjects) {
   1079                 state.updateFullLibraryList();
   1080                 projectList.add(JavaCore.create(state.getProject()));
   1081             }
   1082 
   1083             Job job = new Job("Android Library Update") { //$NON-NLS-1$
   1084                 @Override
   1085                 protected IStatus run(IProgressMonitor monitor) {
   1086                     LibraryClasspathContainerInitializer.updateProjects(
   1087                             projectList.toArray(new IJavaProject[projectList.size()]));
   1088 
   1089                     for (IJavaProject javaProject : projectList) {
   1090                         try {
   1091                             javaProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD,
   1092                                     monitor);
   1093                         } catch (CoreException e) {
   1094                             // pass
   1095                         }
   1096                     }
   1097                     return Status.OK_STATUS;
   1098                 }
   1099             };
   1100             job.setPriority(Job.BUILD);
   1101             job.schedule();
   1102         }
   1103     };
   1104 
   1105     /**
   1106      * Updates all existing projects with a given list of new/updated libraries.
   1107      * This loops through all opened projects and check if they depend on any of the given
   1108      * library project, and if they do, they are linked together.
   1109      * @param libraries the list of new/updated library projects.
   1110      */
   1111     private void updateParentProjects() {
   1112         if (mModifiedChildProjects.size() == 0) {
   1113             return;
   1114         }
   1115 
   1116         ArrayList<ProjectState> childProjects = new ArrayList<ProjectState>(mModifiedChildProjects);
   1117         mModifiedChildProjects.clear();
   1118         synchronized (LOCK) {
   1119             // for each project for which we must update its parent, we loop on the parent
   1120             // projects and adds them to the list of modified projects. If they are themselves
   1121             // libraries, we add them too.
   1122             for (ProjectState state : childProjects) {
   1123                 if (DEBUG) {
   1124                     System.out.println(">>> Updating parents of " + state.getProject().getName());
   1125                 }
   1126                 List<ProjectState> parents = state.getParentProjects();
   1127                 for (ProjectState parent : parents) {
   1128                     markProject(parent, parent.isLibrary());
   1129                 }
   1130                 if (DEBUG) {
   1131                     System.out.println("<<<");
   1132                 }
   1133             }
   1134         }
   1135 
   1136         // done, but there may be parents that are also libraries. Need to update their parents.
   1137         updateParentProjects();
   1138     }
   1139 }
   1140 
   1141