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.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; 22 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceMonitor; 23 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceMonitor.IProjectListener; 24 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge; 25 import com.android.prefs.AndroidLocation.AndroidLocationException; 26 import com.android.sdklib.AndroidVersion; 27 import com.android.sdklib.IAndroidTarget; 28 import com.android.sdklib.ISdkLog; 29 import com.android.sdklib.SdkConstants; 30 import com.android.sdklib.SdkManager; 31 import com.android.sdklib.internal.avd.AvdManager; 32 import com.android.sdklib.internal.project.ApkConfigurationHelper; 33 import com.android.sdklib.internal.project.ApkSettings; 34 import com.android.sdklib.internal.project.ProjectProperties; 35 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 36 37 import org.eclipse.core.resources.IProject; 38 import org.eclipse.core.resources.IncrementalProjectBuilder; 39 import org.eclipse.core.runtime.CoreException; 40 import org.eclipse.core.runtime.IPath; 41 import org.eclipse.core.runtime.IStatus; 42 import org.eclipse.jdt.core.IJavaProject; 43 import org.eclipse.jdt.core.JavaCore; 44 45 import java.io.File; 46 import java.io.IOException; 47 import java.net.MalformedURLException; 48 import java.net.URL; 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.Map; 52 53 /** 54 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used 55 * at the same time. 56 * 57 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of 58 * the Sdk object. 59 * 60 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. 61 */ 62 public class Sdk implements IProjectListener { 63 private static Sdk sCurrentSdk = null; 64 65 private final SdkManager mManager; 66 private final AvdManager mAvdManager; 67 68 private final HashMap<IProject, IAndroidTarget> mProjectTargetMap = 69 new HashMap<IProject, IAndroidTarget>(); 70 private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = 71 new HashMap<IAndroidTarget, AndroidTargetData>(); 72 private final HashMap<IProject, ApkSettings> mApkSettingsMap = 73 new HashMap<IProject, ApkSettings>(); 74 private final String mDocBaseUrl; 75 76 private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager(); 77 78 /** 79 * Classes implementing this interface will receive notification when targets are changed. 80 */ 81 public interface ITargetChangeListener { 82 /** 83 * Sent when project has its target changed. 84 */ onProjectTargetChange(IProject changedProject)85 void onProjectTargetChange(IProject changedProject); 86 87 /** 88 * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, 89 * or the SDK is changed). 90 */ onTargetsLoaded()91 void onTargetsLoaded(); 92 } 93 94 /** 95 * Loads an SDK and returns an {@link Sdk} object if success. 96 * @param sdkLocation the OS path to the SDK. 97 */ loadSdk(String sdkLocation)98 public static Sdk loadSdk(String sdkLocation) { 99 if (sCurrentSdk != null) { 100 sCurrentSdk.dispose(); 101 sCurrentSdk = null; 102 } 103 104 final ArrayList<String> logMessages = new ArrayList<String>(); 105 ISdkLog log = new ISdkLog() { 106 public void error(Throwable throwable, String errorFormat, Object... arg) { 107 if (errorFormat != null) { 108 logMessages.add(String.format("Error: " + errorFormat, arg)); 109 } 110 111 if (throwable != null) { 112 logMessages.add(throwable.getMessage()); 113 } 114 } 115 116 public void warning(String warningFormat, Object... arg) { 117 logMessages.add(String.format("Warning: " + warningFormat, arg)); 118 } 119 120 public void printf(String msgFormat, Object... arg) { 121 logMessages.add(String.format(msgFormat, arg)); 122 } 123 }; 124 125 // get an SdkManager object for the location 126 SdkManager manager = SdkManager.createManager(sdkLocation, log); 127 if (manager != null) { 128 AvdManager avdManager = null; 129 try { 130 avdManager = new AvdManager(manager, log); 131 } catch (AndroidLocationException e) { 132 log.error(e, "Error parsing the AVDs"); 133 } 134 sCurrentSdk = new Sdk(manager, avdManager); 135 return sCurrentSdk; 136 } else { 137 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); 138 for (String msg : logMessages) { 139 sb.append('\n'); 140 sb.append(msg); 141 } 142 AdtPlugin.displayError("Android SDK", sb.toString()); 143 } 144 return null; 145 } 146 147 /** 148 * Returns the current {@link Sdk} object. 149 */ getCurrent()150 public static Sdk getCurrent() { 151 return sCurrentSdk; 152 } 153 154 /** 155 * Returns the location (OS path) of the current SDK. 156 */ getSdkLocation()157 public String getSdkLocation() { 158 return mManager.getLocation(); 159 } 160 161 /** 162 * Returns the URL to the local documentation. 163 * Can return null if no documentation is found in the current SDK. 164 * 165 * @return A file:// URL on the local documentation folder if it exists or null. 166 */ getDocumentationBaseUrl()167 public String getDocumentationBaseUrl() { 168 return mDocBaseUrl; 169 } 170 171 /** 172 * Returns the list of targets that are available in the SDK. 173 */ getTargets()174 public IAndroidTarget[] getTargets() { 175 return mManager.getTargets(); 176 } 177 178 /** 179 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. 180 * 181 * @param hash the {@link IAndroidTarget} hash string. 182 * @return The matching {@link IAndroidTarget} or null. 183 */ getTargetFromHashString(String hash)184 public IAndroidTarget getTargetFromHashString(String hash) { 185 return mManager.getTargetFromHashString(hash); 186 } 187 188 /** 189 * Sets a new target and a new list of Apk configuration for a given project. 190 * 191 * @param project the project to receive the new apk configurations 192 * @param target The new target to set, or <code>null</code> to not change the current target. 193 * @param apkConfigMap a map of apk configurations. The map contains (name, filter) where name 194 * is the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of 195 * resource configuration to include in the apk (see aapt -c). Can be <code>null</code> if the 196 * apk configurations should not be updated. 197 */ setProject(IProject project, IAndroidTarget target, ApkSettings settings)198 public void setProject(IProject project, IAndroidTarget target, 199 ApkSettings settings) { 200 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 201 boolean resolveProject = false; 202 203 ProjectProperties properties = ProjectProperties.load( 204 project.getLocation().toOSString(), PropertyType.DEFAULT); 205 if (properties == null) { 206 // doesn't exist yet? we create it. 207 properties = ProjectProperties.create(project.getLocation().toOSString(), 208 PropertyType.DEFAULT); 209 } 210 211 if (target != null) { 212 // look for the current target of the project 213 IAndroidTarget previousTarget = mProjectTargetMap.get(project); 214 215 if (target != previousTarget) { 216 // save the target hash string in the project persistent property 217 properties.setAndroidTarget(target); 218 219 // put it in a local map for easy access. 220 mProjectTargetMap.put(project, target); 221 222 resolveProject = true; 223 } 224 } 225 226 // if there's no settings, force default values (to reset possibly changed 227 // values in a previous call. 228 if (settings == null) { 229 settings = new ApkSettings(); 230 } 231 232 // save the project settings into the project persistent property 233 ApkConfigurationHelper.setProperties(properties, settings); 234 235 // put it in a local map for easy access. 236 mApkSettingsMap.put(project, settings); 237 238 // we are done with the modification. Save the property file. 239 try { 240 properties.save(); 241 } catch (IOException e) { 242 AdtPlugin.log(e, "Failed to save default.properties for project '%s'", 243 project.getName()); 244 } 245 246 if (resolveProject) { 247 // force a resolve of the project by updating the classpath container. 248 // This will also force a recompile. 249 IJavaProject javaProject = JavaCore.create(project); 250 AndroidClasspathContainerInitializer.updateProjects( 251 new IJavaProject[] { javaProject }); 252 } else { 253 // always do a full clean/build. 254 try { 255 project.build(IncrementalProjectBuilder.CLEAN_BUILD, null); 256 } catch (CoreException e) { 257 // failed to build? force resolve instead. 258 IJavaProject javaProject = JavaCore.create(project); 259 AndroidClasspathContainerInitializer.updateProjects( 260 new IJavaProject[] { javaProject }); 261 } 262 } 263 264 // finally, update the opened editors. 265 if (resolveProject) { 266 AdtPlugin.getDefault().updateTargetListener(project); 267 } 268 } 269 } 270 271 /** 272 * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. 273 */ getTarget(IProject project)274 public IAndroidTarget getTarget(IProject project) { 275 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 276 IAndroidTarget target = mProjectTargetMap.get(project); 277 if (target == null) { 278 // get the value from the project persistent property. 279 String targetHashString = loadProjectProperties(project, this); 280 281 if (targetHashString != null) { 282 target = mManager.getTargetFromHashString(targetHashString); 283 } 284 } 285 286 return target; 287 } 288 } 289 290 291 /** 292 * Parses the project properties and returns the hash string uniquely identifying the 293 * target of the given project. 294 * <p/> 295 * This methods reads the content of the <code>default.properties</code> file present in 296 * the root folder of the project. 297 * <p/>The returned string is equivalent to the return of {@link IAndroidTarget#hashString()}. 298 * @param project The project for which to return the target hash string. 299 * @param sdkStorage The sdk in which to store the Apk Configs. Can be null. 300 * @return the hash string or null if the project does not have a target set. 301 */ loadProjectProperties(IProject project, Sdk sdkStorage)302 private static String loadProjectProperties(IProject project, Sdk sdkStorage) { 303 // load the default.properties from the project folder. 304 IPath location = project.getLocation(); 305 if (location == null) { // can return null when the project is being deleted. 306 // do nothing and return null; 307 return null; 308 } 309 ProjectProperties properties = ProjectProperties.load(location.toOSString(), 310 PropertyType.DEFAULT); 311 if (properties == null) { 312 AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'", 313 project.getName()); 314 return null; 315 } 316 317 if (sdkStorage != null) { 318 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 319 ApkSettings settings = ApkConfigurationHelper.getSettings(properties); 320 321 if (settings != null) { 322 sdkStorage.mApkSettingsMap.put(project, settings); 323 } 324 } 325 } 326 327 return properties.getProperty(ProjectProperties.PROPERTY_TARGET); 328 } 329 330 /** 331 * Returns the hash string uniquely identifying the target of a project. 332 * <p/> 333 * This methods reads the content of the <code>default.properties</code> file present in 334 * the root folder of the project. 335 * <p/>The string is equivalent to the return of {@link IAndroidTarget#hashString()}. 336 * @param project The project for which to return the target hash string. 337 * @return the hash string or null if the project does not have a target set. 338 */ getProjectTargetHashString(IProject project)339 public static String getProjectTargetHashString(IProject project) { 340 return loadProjectProperties(project, null /*storeConfigs*/); 341 } 342 343 /** 344 * Sets a target hash string in given project's <code>default.properties</code> file. 345 * @param project The project in which to save the hash string. 346 * @param targetHashString The target hash string to save. This must be the result from 347 * {@link IAndroidTarget#hashString()}. 348 */ setProjectTargetHashString(IProject project, String targetHashString)349 public static void setProjectTargetHashString(IProject project, String targetHashString) { 350 // because we don't want to erase other properties from default.properties, we first load 351 // them 352 ProjectProperties properties = ProjectProperties.load(project.getLocation().toOSString(), 353 PropertyType.DEFAULT); 354 if (properties == null) { 355 // doesn't exist yet? we create it. 356 properties = ProjectProperties.create(project.getLocation().toOSString(), 357 PropertyType.DEFAULT); 358 } 359 360 // add/change the target hash string. 361 properties.setProperty(ProjectProperties.PROPERTY_TARGET, targetHashString); 362 363 // and rewrite the file. 364 try { 365 properties.save(); 366 } catch (IOException e) { 367 AdtPlugin.log(e, "Failed to save default.properties for project '%s'", 368 project.getName()); 369 } 370 } 371 /** 372 * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. 373 */ getTargetData(IAndroidTarget target)374 public AndroidTargetData getTargetData(IAndroidTarget target) { 375 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 376 return mTargetDataMap.get(target); 377 } 378 } 379 380 /** 381 * Return the {@link AndroidTargetData} for a given {@link IProject}. 382 */ getTargetData(IProject project)383 public AndroidTargetData getTargetData(IProject project) { 384 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 385 IAndroidTarget target = getTarget(project); 386 if (target != null) { 387 return getTargetData(target); 388 } 389 } 390 391 return null; 392 } 393 394 /** 395 * Returns the APK settings for a given project. 396 */ getApkSettings(IProject project)397 public ApkSettings getApkSettings(IProject project) { 398 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 399 return mApkSettingsMap.get(project); 400 } 401 } 402 403 /** 404 * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could 405 * be <code>null</code>. 406 */ getAvdManager()407 public AvdManager getAvdManager() { 408 return mAvdManager; 409 } 410 getDeviceVersion(IDevice device)411 public static AndroidVersion getDeviceVersion(IDevice device) { 412 try { 413 Map<String, String> props = device.getProperties(); 414 String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL); 415 if (apiLevel == null) { 416 return null; 417 } 418 419 return new AndroidVersion(Integer.parseInt(apiLevel), 420 props.get((IDevice.PROP_BUILD_CODENAME))); 421 } catch (NumberFormatException e) { 422 return null; 423 } 424 } 425 getLayoutDeviceManager()426 public LayoutDeviceManager getLayoutDeviceManager() { 427 return mLayoutDeviceManager; 428 } 429 Sdk(SdkManager manager, AvdManager avdManager)430 private Sdk(SdkManager manager, AvdManager avdManager) { 431 mManager = manager; 432 mAvdManager = avdManager; 433 434 // listen to projects closing 435 ResourceMonitor monitor = ResourceMonitor.getMonitor(); 436 monitor.addProjectListener(this); 437 438 // pre-compute some paths 439 mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + 440 SdkConstants.OS_SDK_DOCS_FOLDER); 441 442 // load the built-in and user layout devices 443 mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation()); 444 // and the ones from the add-on 445 loadLayoutDevices(); 446 } 447 448 /** 449 * Cleans and unloads the SDK. 450 */ dispose()451 private void dispose() { 452 ResourceMonitor.getMonitor().removeProjectListener(this); 453 } 454 setTargetData(IAndroidTarget target, AndroidTargetData data)455 void setTargetData(IAndroidTarget target, AndroidTargetData data) { 456 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 457 mTargetDataMap.put(target, data); 458 } 459 } 460 461 /** 462 * Returns the URL to the local documentation. 463 * Can return null if no documentation is found in the current SDK. 464 * 465 * @param osDocsPath Path to the documentation folder in the current SDK. 466 * The folder may not actually exist. 467 * @return A file:// URL on the local documentation folder if it exists or null. 468 */ getDocumentationBaseUrl(String osDocsPath)469 private String getDocumentationBaseUrl(String osDocsPath) { 470 File f = new File(osDocsPath); 471 472 if (f.isDirectory()) { 473 try { 474 // Note: to create a file:// URL, one would typically use something like 475 // f.toURI().toURL().toString(). However this generates a broken path on 476 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of 477 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll 478 // do the correct thing manually. 479 480 String path = f.getAbsolutePath(); 481 if (File.separatorChar != '/') { 482 path = path.replace(File.separatorChar, '/'); 483 } 484 485 // For some reason the URL class doesn't add the mandatory "//" after 486 // the "file:" protocol name, so it has to be hacked into the path. 487 URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ 488 String result = url.toString(); 489 return result; 490 } catch (MalformedURLException e) { 491 // ignore malformed URLs 492 } 493 } 494 495 return null; 496 } 497 498 /** 499 * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to 500 * load {@link LayoutDevice} from them. 501 */ loadLayoutDevices()502 private void loadLayoutDevices() { 503 IAndroidTarget[] targets = mManager.getTargets(); 504 for (IAndroidTarget target : targets) { 505 if (target.isPlatform() == false) { 506 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML); 507 if (deviceXml.isFile()) { 508 mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml); 509 } 510 } 511 } 512 513 mLayoutDeviceManager.sealAddonLayoutDevices(); 514 } 515 projectClosed(IProject project)516 public void projectClosed(IProject project) { 517 // get the target project 518 synchronized (AdtPlugin.getDefault().getSdkLockObject()) { 519 IAndroidTarget target = mProjectTargetMap.get(project); 520 if (target != null) { 521 // get the bridge for the target, and clear the cache for this project. 522 AndroidTargetData data = mTargetDataMap.get(target); 523 if (data != null) { 524 LayoutBridge bridge = data.getLayoutBridge(); 525 if (bridge != null && bridge.status == LoadStatus.LOADED) { 526 bridge.bridge.clearCaches(project); 527 } 528 } 529 } 530 531 // now remove the project for the maps. 532 mProjectTargetMap.remove(project); 533 mApkSettingsMap.remove(project); 534 } 535 } 536 projectDeleted(IProject project)537 public void projectDeleted(IProject project) { 538 projectClosed(project); 539 } 540 projectOpened(IProject project)541 public void projectOpened(IProject project) { 542 // ignore this. The project will be added to the map the first time the target needs 543 // to be resolved. 544 } 545 projectOpenedWithWorkspace(IProject project)546 public void projectOpenedWithWorkspace(IProject project) { 547 // ignore this. The project will be added to the map the first time the target needs 548 // to be resolved. 549 } 550 } 551 552