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