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
setActivePart(IAction action, IWorkbenchPart targetPart)103     public void setActivePart(IAction action, IWorkbenchPart targetPart) {
104     }
105 
106     @Override
run(IAction action)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
selectionChanged(IAction action, ISelection selection)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      */
install(final IProject project)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
installSupport(int minimumRevision)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      */
getInstalledRevision()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      */
installGridLayoutLibrary(final IProject project, boolean waitForFinish)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      */
installAppCompatLibrary(final IProject project, boolean waitForFinish)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      */
getSupportPackageDir()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
getSupportJarFile()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
getSupport13JarFile()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      */
createLibraryProject( final File libraryPath, final IProject project, final String libraryName, boolean waitForFinish)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      */
addLibraryDependency( final IProject libraryProject, final IProject dependentProject, boolean waitForFinish)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 
copyJarIntoProject( IProject project, File jarPath)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      */
init(IWorkbenchWindow window)622     public void init(IWorkbenchWindow window) {
623         // pass
624     }
625 
626 }
627