1 /* 2 * Copyright (C) 2011 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.actions; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.Nullable; 21 import com.android.ide.eclipse.adt.AdtConstants; 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.ide.eclipse.adt.AdtUtils; 24 import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog; 25 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 26 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 27 import com.android.sdklib.SdkManager; 28 import com.android.sdklib.internal.project.ProjectProperties; 29 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 30 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; 31 import com.android.sdklib.io.FileOp; 32 import com.android.sdkuilib.internal.repository.ui.AdtUpdateDialog; 33 import com.android.utils.NullLogger; 34 import com.android.utils.Pair; 35 36 import org.eclipse.core.filesystem.EFS; 37 import org.eclipse.core.filesystem.IFileStore; 38 import org.eclipse.core.filesystem.IFileSystem; 39 import org.eclipse.core.resources.IFile; 40 import org.eclipse.core.resources.IFolder; 41 import org.eclipse.core.resources.IProject; 42 import org.eclipse.core.resources.IProjectDescription; 43 import org.eclipse.core.resources.IResource; 44 import org.eclipse.core.resources.IWorkspace; 45 import org.eclipse.core.resources.IWorkspaceRoot; 46 import org.eclipse.core.resources.ResourcesPlugin; 47 import org.eclipse.core.runtime.CoreException; 48 import org.eclipse.core.runtime.IAdaptable; 49 import org.eclipse.core.runtime.IPath; 50 import org.eclipse.core.runtime.IProgressMonitor; 51 import org.eclipse.core.runtime.IStatus; 52 import org.eclipse.core.runtime.NullProgressMonitor; 53 import org.eclipse.core.runtime.Status; 54 import org.eclipse.core.runtime.jobs.Job; 55 import org.eclipse.jdt.core.IJavaProject; 56 import org.eclipse.jdt.core.JavaCore; 57 import org.eclipse.jface.action.IAction; 58 import org.eclipse.jface.viewers.ISelection; 59 import org.eclipse.jface.viewers.IStructuredSelection; 60 import org.eclipse.ui.IObjectActionDelegate; 61 import org.eclipse.ui.IWorkbenchPart; 62 import org.eclipse.ui.IWorkbenchWindow; 63 import org.eclipse.ui.IWorkbenchWindowActionDelegate; 64 65 import java.io.File; 66 import java.io.IOException; 67 import java.util.Iterator; 68 import java.util.Map; 69 70 /** 71 * An action to add the android-support-v4.jar support library 72 * to the selected project. 73 * <p/> 74 * This should be used by the GLE. The action itself is currently more 75 * like an example of how to invoke the new {@link AdtUpdateDialog}. 76 * <p/> 77 * TODO: make this more configurable. 78 */ 79 public class AddSupportJarAction implements IObjectActionDelegate { 80 81 /** The vendor ID of the support library. */ 82 private static final String VENDOR_ID = "android"; //$NON-NLS-1$ 83 /** The path ID of the support library. */ 84 private static final String SUPPORT_ID = "support"; //$NON-NLS-1$ 85 /** The path ID of the compatibility library (which was its id for releases 1-3). */ 86 private static final String COMPATIBILITY_ID = "compatibility"; //$NON-NLS-1$ 87 private static final String FD_GRIDLAYOUT = "gridlayout"; //$NON-NLS-1$ 88 private static final String FD_V7 = "v7"; //$NON-NLS-1$ 89 private static final String FD_V4 = "v4"; //$NON-NLS-1$ 90 private static final String FD_V13 = "v13"; //$NON-NLS-1$ 91 private static final String FD_APPCOMPAT = "appcompat"; //$NON-NLS-1$ 92 private static final String FD_LIBS = "libs"; //$NON-NLS-1$ 93 private static final String ANDROID_SUPPORT_V4_JAR = "android-support-v4.jar"; //$NON-NLS-1$ 94 private static final String ANDROID_SUPPORT_V13_JAR = "android-support-v13.jar";//$NON-NLS-1$ 95 private static final String APPCOMPAT_V7_JAR = "android-support-v7-appcompat.jar";//$NON-NLS-1$ 96 private static final String APP_COMPAT_LIB_NAME = "appcompat_v7"; //$NON-NLS-1$ 97 private ISelection mSelection; 98 99 /** 100 * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) 101 */ 102 @Override 103 public void setActivePart(IAction action, IWorkbenchPart targetPart) { 104 } 105 106 @Override 107 public void run(IAction action) { 108 if (mSelection instanceof IStructuredSelection) { 109 110 for (Iterator<?> it = ((IStructuredSelection) mSelection).iterator(); 111 it.hasNext();) { 112 Object element = it.next(); 113 IProject project = null; 114 if (element instanceof IProject) { 115 project = (IProject) element; 116 } else if (element instanceof IAdaptable) { 117 project = (IProject) ((IAdaptable) element) 118 .getAdapter(IProject.class); 119 } 120 if (project != null) { 121 install(project); 122 } 123 } 124 } 125 } 126 127 @Override 128 public void selectionChanged(IAction action, ISelection selection) { 129 mSelection = selection; 130 } 131 132 /** 133 * Install the support jar into the given project. 134 * 135 * @param project The Android project to install the support jar into 136 * @return true if the installation was successful 137 */ 138 public static boolean install(final IProject project) { 139 File jarPath = installSupport(-1); 140 if (jarPath != null) { 141 try { 142 return copyJarIntoProject(project, jarPath) != null; 143 } catch (Exception e) { 144 AdtPlugin.log(e, null); 145 } 146 } 147 148 return false; 149 } 150 151 /** 152 * Installs the Android Support library into the SDK extras/ folder. If a minimum 153 * revision number is specified, this method will check whether the package is already 154 * installed, and if the installed revision is at least as high as the requested revision, 155 * this method will exit without performing an update. 156 * 157 * @param minimumRevision a minimum revision, or -1 to upgrade 158 * unconditionally. Note that this does <b>NOT</b> specify which 159 * revision to install; the latest version will always be 160 * installed. 161 * @return the location of the support jar file, or null if something went 162 * wrong 163 */ 164 @Nullable 165 public static File installSupport(int minimumRevision) { 166 167 final Sdk sdk = Sdk.getCurrent(); 168 if (sdk == null) { 169 AdtPlugin.printErrorToConsole( 170 AddSupportJarAction.class.getSimpleName(), // tag 171 "Error: Android SDK is not loaded yet."); //$NON-NLS-1$ 172 return null; 173 } 174 175 String sdkLocation = sdk.getSdkOsLocation(); 176 if (minimumRevision > 0) { 177 File path = getSupportJarFile(); 178 if (path != null) { 179 assert path.exists(); // guaranteed by the getSupportJarFile call 180 int installedRevision = getInstalledRevision(); 181 if (installedRevision != -1 && minimumRevision <= installedRevision) { 182 return path; 183 } 184 } 185 } 186 187 // TODO: For the generic action, check the library isn't in the project already. 188 189 // First call the package manager to make sure the package is installed 190 // and get the installation path of the library. 191 192 AdtUpdateDialog window = new AdtUpdateDialog( 193 AdtPlugin.getShell(), 194 new AdtConsoleSdkLog(), 195 sdkLocation); 196 197 Pair<Boolean, File> result = window.installExtraPackage(VENDOR_ID, SUPPORT_ID); 198 199 // TODO: Make sure the version is at the required level; we know we need at least one 200 // containing the v7 support 201 202 if (!result.getFirst().booleanValue()) { 203 AdtPlugin.printErrorToConsole("Failed to install Android Support library"); 204 return null; 205 } 206 207 // TODO these "v4" values needs to be dynamic, e.g. we could try to match 208 // vN/android-support-vN.jar. Eventually we'll want to rely on info from the 209 // package manifest anyway so this is irrelevant. 210 211 File path = new File(result.getSecond(), FD_V4); 212 final File jarPath = new File(path, ANDROID_SUPPORT_V4_JAR); 213 214 if (!jarPath.isFile()) { 215 AdtPlugin.printErrorToConsole("Android Support Jar not found:", 216 jarPath.getAbsolutePath()); 217 return null; 218 } 219 220 return jarPath; 221 } 222 223 /** 224 * Returns the installed revision number of the Android Support 225 * library, or -1 if the package is not installed. 226 * 227 * @return the installed revision number, or -1 228 */ 229 public static int getInstalledRevision() { 230 final Sdk sdk = Sdk.getCurrent(); 231 if (sdk != null) { 232 String sdkLocation = sdk.getSdkOsLocation(); 233 SdkManager manager = SdkManager.createManager(sdkLocation, NullLogger.getLogger()); 234 Map<String, Integer> versions = manager.getExtrasVersions(); 235 Integer version = versions.get(VENDOR_ID + '/' + SUPPORT_ID); 236 if (version == null) { 237 // Check the old compatibility library. When the library is updated in-place 238 // the manager doesn't change its folder name (since that is a source of 239 // endless issues on Windows.) 240 version = versions.get(VENDOR_ID + '/' + COMPATIBILITY_ID); 241 } 242 if (version != null) { 243 return version.intValue(); 244 } 245 } 246 247 return -1; 248 } 249 250 /** 251 * Similar to {@link #install}, but rather than copy a jar into the given 252 * project, it creates a new library project in the workspace for the 253 * support library, and adds a library dependency on the newly 254 * installed library from the given project. 255 * 256 * @param project the project to add a dependency on the library to 257 * @param waitForFinish If true, block until the task has finished 258 * @return true if the installation was successful (or if 259 * <code>waitForFinish</code> is false, if the installation is 260 * likely to be successful - e.g. the user has at least agreed to 261 * all installation prompts.) 262 */ 263 public static boolean installGridLayoutLibrary(final IProject project, boolean waitForFinish) { 264 final IJavaProject javaProject = JavaCore.create(project); 265 if (javaProject != null) { 266 267 File supportPath = getSupportPackageDir(); 268 if (!supportPath.isDirectory()) { 269 File path = installSupport(8); // GridLayout arrived in rev 7 and fixed in rev 8 270 if (path == null) { 271 return false; 272 } 273 assert path.equals(supportPath); 274 } 275 File libraryPath = new File(supportPath, FD_V7 + File.separator + FD_GRIDLAYOUT); 276 if (!libraryPath.isDirectory()) { 277 // Upgrade support package: it's out of date. The SDK manager will 278 // perform an upgrade to the latest version if the package is already installed. 279 File path = installSupport(-1); 280 if (path == null) { 281 return false; 282 } 283 assert path.equals(libraryPath) : path; 284 } 285 286 // Create workspace copy of the project and add library dependency 287 IProject libraryProject = createLibraryProject(libraryPath, project, 288 "gridlayout_v7", waitForFinish); //$NON-NLS-1$ 289 if (libraryProject != null) { 290 return addLibraryDependency(libraryProject, project, waitForFinish); 291 } 292 } 293 294 return false; 295 } 296 297 /** 298 * Similar to {@link #install}, but rather than copy a jar into the given 299 * project, it creates a new library project in the workspace for the 300 * support library, and adds a library dependency on the newly 301 * installed library from the given project. 302 * 303 * @param project the project to add a dependency on the library to 304 * @param waitForFinish If true, block until the task has finished 305 * @return true if the installation was successful (or if 306 * <code>waitForFinish</code> is false, if the installation is 307 * likely to be successful - e.g. the user has at least agreed to 308 * all installation prompts.) 309 */ 310 public static boolean installAppCompatLibrary(final IProject project, boolean waitForFinish) { 311 final IJavaProject javaProject = JavaCore.create(project); 312 if (javaProject != null) { 313 314 // Don't add in the library if it already exists 315 ProjectState state = Sdk.getProjectState(project); 316 ProjectPropertiesWorkingCopy copy = state.getProperties().makeWorkingCopy(); 317 for (String property : copy.keySet()) { 318 if (property.startsWith(ProjectProperties.PROPERTY_LIB_REF)) { 319 String libraryReference = copy.getProperty(property); 320 if (libraryReference != null && libraryReference.contains(APP_COMPAT_LIB_NAME)) { 321 return true; 322 } 323 } 324 } 325 326 File supportPath = getSupportPackageDir(); 327 if (!supportPath.isDirectory()) { 328 File path = installSupport(7); 329 if (path == null) { 330 return false; 331 } 332 assert path.equals(supportPath); 333 } 334 File libraryPath = new File(supportPath, FD_V7 + File.separator + FD_APPCOMPAT); 335 if (!libraryPath.isDirectory()) { 336 // Upgrade support package: it's out of date. The SDK manager will 337 // perform an upgrade to the latest version if the package is already installed. 338 File path = installSupport(-1); 339 if (path == null) { 340 return false; 341 } 342 assert path.equals(libraryPath) : path; 343 } 344 345 // Check to see if there's already a version of the library available 346 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 347 IWorkspaceRoot root = workspace.getRoot(); 348 IProject libraryProject = root.getProject(APP_COMPAT_LIB_NAME); 349 if (!libraryProject.exists()) { 350 // Create workspace copy of the project and add library dependency 351 libraryProject = createLibraryProject(libraryPath, project, 352 APP_COMPAT_LIB_NAME, waitForFinish); 353 } 354 if (libraryProject != null) { 355 return addLibraryDependency(libraryProject, project, waitForFinish); 356 } 357 } 358 359 return false; 360 } 361 362 /** 363 * Returns the directory containing the support libraries (v4, v7, v13, 364 * ...), which may or may not exist 365 * 366 * @return a path to the support library or null 367 */ 368 private static File getSupportPackageDir() { 369 final Sdk sdk = Sdk.getCurrent(); 370 if (sdk != null) { 371 String sdkLocation = sdk.getSdkOsLocation(); 372 SdkManager manager = SdkManager.createManager(sdkLocation, NullLogger.getLogger()); 373 Map<String, Integer> versions = manager.getExtrasVersions(); 374 Integer version = versions.get(VENDOR_ID + '/' + SUPPORT_ID); 375 if (version != null) { 376 File supportPath = new File(sdkLocation, 377 SdkConstants.FD_EXTRAS + File.separator 378 + VENDOR_ID + File.separator 379 + SUPPORT_ID); 380 return supportPath; 381 } 382 383 // Check the old compatibility library. When the library is updated in-place 384 // the manager doesn't change its folder name (since that is a source of 385 // endless issues on Windows.) 386 version = versions.get(VENDOR_ID + '/' + COMPATIBILITY_ID); 387 if (version != null) { 388 File supportPath = new File(sdkLocation, 389 SdkConstants.FD_EXTRAS + File.separator 390 + VENDOR_ID + File.separator 391 + COMPATIBILITY_ID); 392 return supportPath; 393 } 394 } 395 return null; 396 } 397 398 /** 399 * Returns a path to the installed jar file for the support library, 400 * or null if it does not exist 401 * 402 * @return a path to the v4.jar or null 403 */ 404 @Nullable 405 public static File getSupportJarFile() { 406 File supportDir = getSupportPackageDir(); 407 if (supportDir != null) { 408 File path = new File(supportDir, FD_V4 + File.separator + ANDROID_SUPPORT_V4_JAR); 409 if (path.exists()) { 410 return path; 411 } 412 } 413 414 return null; 415 } 416 417 /** 418 * Returns a path to the installed jar file for the support library, 419 * or null if it does not exist 420 * 421 * @return a path to the v13.jar or null 422 */ 423 @Nullable 424 public static File getSupport13JarFile() { 425 File supportDir = getSupportPackageDir(); 426 if (supportDir != null) { 427 File path = new File(supportDir, FD_V13 + File.separator + ANDROID_SUPPORT_V13_JAR); 428 if (path.exists()) { 429 return path; 430 } 431 } 432 433 return null; 434 } 435 436 /** 437 * Creates a library project in the Eclipse workspace out of the grid layout project 438 * in the SDK tree. 439 * 440 * @param libraryPath the path to the directory tree containing the project contents 441 * @param project the project to copy the SDK target out of 442 * @param waitForFinish whether the operation should finish before this method returns 443 * @return a library project, or null if it fails for some reason 444 */ 445 private static IProject createLibraryProject( 446 final File libraryPath, 447 final IProject project, 448 final String libraryName, 449 boolean waitForFinish) { 450 451 // Install a new library into the workspace. This is a copy rather than 452 // a reference to the support library version such that modifications 453 // do not modify the pristine copy in the SDK install area. 454 455 final IProject newProject; 456 try { 457 IProgressMonitor monitor = new NullProgressMonitor(); 458 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 459 IWorkspaceRoot root = workspace.getRoot(); 460 461 String name = AdtUtils.getUniqueProjectName( 462 libraryName, "_"); //$NON-NLS-1$ 463 newProject = root.getProject(name); 464 IProjectDescription description = workspace.newProjectDescription(name); 465 String[] natures = new String[] { AdtConstants.NATURE_DEFAULT, JavaCore.NATURE_ID }; 466 description.setNatureIds(natures); 467 newProject.create(description, monitor); 468 469 // Copy in the files recursively 470 IFileSystem fileSystem = EFS.getLocalFileSystem(); 471 IFileStore sourceDir = fileSystem.getStore(libraryPath.toURI()); 472 IFileStore destDir = fileSystem.getStore(newProject.getLocationURI()); 473 sourceDir.copy(destDir, EFS.OVERWRITE, null); 474 475 // Make sure the src folder exists 476 destDir.getChild(SdkConstants.SRC_FOLDER).mkdir(0, null /*monitor*/); 477 478 // Set the android platform to the same level as the calling project 479 ProjectState state = Sdk.getProjectState(project); 480 String target = state.getProperties().getProperty(ProjectProperties.PROPERTY_TARGET); 481 if (target != null && target.length() > 0) { 482 ProjectProperties properties = ProjectProperties.load( 483 destDir.toLocalFile(EFS.NONE, new NullProgressMonitor()).getPath(), 484 PropertyType.PROJECT); 485 ProjectPropertiesWorkingCopy copy = properties.makeWorkingCopy(); 486 copy.setProperty(ProjectProperties.PROPERTY_TARGET, target); 487 try { 488 copy.save(); 489 } catch (Exception e) { 490 AdtPlugin.log(e, null); 491 } 492 } 493 494 newProject.open(monitor); 495 496 return newProject; 497 } catch (CoreException e) { 498 AdtPlugin.log(e, null); 499 return null; 500 } 501 } 502 503 /** 504 * Adds a library dependency on the given library into the given project. 505 * 506 * @param libraryProject the library project to depend on 507 * @param dependentProject the project to write the dependency into 508 * @param waitForFinish whether this method should wait for the job to 509 * finish 510 * @return true if the operation succeeded 511 */ 512 public static boolean addLibraryDependency( 513 final IProject libraryProject, 514 final IProject dependentProject, 515 boolean waitForFinish) { 516 517 // Now add library dependency 518 519 // Run an Eclipse asynchronous job to update the project 520 Job job = new Job("Add Support Library Dependency to Project") { 521 @Override 522 protected IStatus run(IProgressMonitor monitor) { 523 try { 524 monitor.beginTask("Add library dependency to project build path", 3); 525 monitor.worked(1); 526 527 // TODO: Add library project to the project.properties file! 528 ProjectState state = Sdk.getProjectState(dependentProject); 529 ProjectPropertiesWorkingCopy mPropertiesWorkingCopy = 530 state.getProperties().makeWorkingCopy(); 531 532 // Get the highest version number of the libraries; there cannot be any 533 // gaps so we will assign the next library the next number 534 int nextVersion = 1; 535 for (String property : mPropertiesWorkingCopy.keySet()) { 536 if (property.startsWith(ProjectProperties.PROPERTY_LIB_REF)) { 537 String s = property.substring( 538 ProjectProperties.PROPERTY_LIB_REF.length()); 539 int version = Integer.parseInt(s); 540 if (version >= nextVersion) { 541 nextVersion = version + 1; 542 } 543 } 544 } 545 546 IPath relativePath = libraryProject.getLocation().makeRelativeTo( 547 dependentProject.getLocation()); 548 549 mPropertiesWorkingCopy.setProperty( 550 ProjectProperties.PROPERTY_LIB_REF + nextVersion, 551 relativePath.toString()); 552 try { 553 mPropertiesWorkingCopy.save(); 554 IResource projectProp = dependentProject.findMember( 555 SdkConstants.FN_PROJECT_PROPERTIES); 556 projectProp.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); 557 } catch (Exception e) { 558 String msg = String.format( 559 "Failed to save %1$s for project %2$s", 560 SdkConstants.FN_PROJECT_PROPERTIES, dependentProject.getName()); 561 AdtPlugin.log(e, msg); 562 } 563 564 // Project fix-ups 565 Job fix = FixProjectAction.createFixProjectJob(libraryProject); 566 fix.schedule(); 567 fix.join(); 568 569 monitor.worked(1); 570 571 return Status.OK_STATUS; 572 } catch (Exception e) { 573 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, 574 "Failed", e); //$NON-NLS-1$ 575 } finally { 576 if (monitor != null) { 577 monitor.done(); 578 } 579 } 580 } 581 }; 582 job.schedule(); 583 584 if (waitForFinish) { 585 try { 586 job.join(); 587 return job.getState() == IStatus.OK; 588 } catch (InterruptedException e) { 589 AdtPlugin.log(e, null); 590 } 591 } 592 593 return true; 594 } 595 596 private static IResource copyJarIntoProject( 597 IProject project, 598 File jarPath) throws IOException, CoreException { 599 IFolder resFolder = project.getFolder(SdkConstants.FD_NATIVE_LIBS); 600 if (!resFolder.exists()) { 601 resFolder.create(IResource.FORCE, true /*local*/, null); 602 } 603 604 IFile destFile = resFolder.getFile(jarPath.getName()); 605 IPath loc = destFile.getLocation(); 606 File destPath = loc.toFile(); 607 608 // Only modify the file if necessary so that we don't trigger unnecessary recompilations 609 FileOp f = new FileOp(); 610 if (!f.isFile(destPath) || !f.isSameFile(jarPath, destPath)) { 611 f.copyFile(jarPath, destPath); 612 // Make sure Eclipse discovers java.io file changes 613 resFolder.refreshLocal(1, new NullProgressMonitor()); 614 } 615 616 return destFile; 617 } 618 619 /** 620 * @see IWorkbenchWindowActionDelegate#init 621 */ 622 public void init(IWorkbenchWindow window) { 623 // pass 624 } 625 626 } 627