1 /* 2 * Copyright (C) 2007 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.wizards.newproject; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.AndroidConstants; 21 import com.android.ide.eclipse.adt.internal.project.AndroidNature; 22 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 23 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 24 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.IMainInfo; 25 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectCreationPage.TestInfo; 26 import com.android.sdklib.IAndroidTarget; 27 import com.android.sdklib.SdkConstants; 28 import com.android.sdklib.io.StreamException; 29 import com.android.sdklib.resources.Density; 30 31 import org.eclipse.core.resources.IContainer; 32 import org.eclipse.core.resources.IFile; 33 import org.eclipse.core.resources.IFolder; 34 import org.eclipse.core.resources.IProject; 35 import org.eclipse.core.resources.IProjectDescription; 36 import org.eclipse.core.resources.IResource; 37 import org.eclipse.core.resources.IResourceStatus; 38 import org.eclipse.core.resources.IWorkspace; 39 import org.eclipse.core.resources.ResourcesPlugin; 40 import org.eclipse.core.runtime.CoreException; 41 import org.eclipse.core.runtime.IPath; 42 import org.eclipse.core.runtime.IProgressMonitor; 43 import org.eclipse.core.runtime.IStatus; 44 import org.eclipse.core.runtime.OperationCanceledException; 45 import org.eclipse.core.runtime.Platform; 46 import org.eclipse.core.runtime.SubProgressMonitor; 47 import org.eclipse.jdt.core.IAccessRule; 48 import org.eclipse.jdt.core.IClasspathAttribute; 49 import org.eclipse.jdt.core.IClasspathEntry; 50 import org.eclipse.jdt.core.IJavaProject; 51 import org.eclipse.jdt.core.JavaCore; 52 import org.eclipse.jdt.core.JavaModelException; 53 import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; 54 import org.eclipse.jface.dialogs.ErrorDialog; 55 import org.eclipse.jface.dialogs.MessageDialog; 56 import org.eclipse.jface.resource.ImageDescriptor; 57 import org.eclipse.jface.viewers.IStructuredSelection; 58 import org.eclipse.jface.wizard.Wizard; 59 import org.eclipse.ui.INewWizard; 60 import org.eclipse.ui.IWorkbench; 61 import org.eclipse.ui.actions.WorkspaceModifyOperation; 62 63 import java.io.ByteArrayInputStream; 64 import java.io.File; 65 import java.io.FileNotFoundException; 66 import java.io.IOException; 67 import java.io.InputStream; 68 import java.lang.reflect.InvocationTargetException; 69 import java.net.MalformedURLException; 70 import java.util.HashMap; 71 import java.util.Map; 72 import java.util.Set; 73 import java.util.Map.Entry; 74 75 /** 76 * A "New Android Project" Wizard. 77 * <p/> 78 * Note: this class is public so that it can be accessed from unit tests. 79 * It is however an internal class. Its API may change without notice. 80 * It should semantically be considered as a private final class. 81 * Do not derive from this class. 82 83 */ 84 public class NewProjectWizard extends Wizard implements INewWizard { 85 86 /** 87 * Indicates which pages should be available in the New Project Wizard. 88 */ 89 protected enum AvailablePages { 90 /** 91 * Both the usual "Android Project" and the "Android Test Project" pages will 92 * be available. The first page displayed will be the former one and it can depend 93 * on the soon-to-be created normal project. 94 */ 95 ANDROID_AND_TEST_PROJECT, 96 /** 97 * Only the "Android Test Project" page will be available. User will have to 98 * select an existing Android Project. If the selection matches such a project, 99 * it will be used as a default. 100 */ 101 TEST_PROJECT_ONLY 102 } 103 104 private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$ 105 private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$ 106 private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$ 107 private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$ 108 private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$ 109 private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$ 110 private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$ 111 private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$ 112 private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$ 113 private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$ 114 private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$ 115 // Warning: The expanded string PARAM_TEST_TARGET_PACKAGE must not contain the 116 // string "PACKAGE" since it collides with the replacement of PARAM_PACKAGE. 117 private static final String PARAM_TEST_TARGET_PACKAGE = "TEST_TARGET_PCKG"; //$NON-NLS-1$ 118 private static final String PARAM_TARGET_SELF = "TARGET_SELF"; //$NON-NLS-1$ 119 private static final String PARAM_TARGET_MAIN = "TARGET_MAIN"; //$NON-NLS-1$ 120 private static final String PARAM_TARGET_EXISTING = "TARGET_EXISTING"; //$NON-NLS-1$ 121 private static final String PARAM_REFERENCE_PROJECT = "REFERENCE_PROJECT"; //$NON-NLS-1$ 122 123 private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$ 124 private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$ 125 private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$ 126 private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$ 127 private static final String PH_TEST_USES_LIBRARY = "TEST-USES-LIBRARY"; //$NON-NLS-1$ 128 private static final String PH_TEST_INSTRUMENTATION = "TEST-INSTRUMENTATION"; //$NON-NLS-1$ 129 130 private static final String BIN_DIRECTORY = 131 SdkConstants.FD_OUTPUT + AndroidConstants.WS_SEP; 132 private static final String RES_DIRECTORY = 133 SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; 134 private static final String ASSETS_DIRECTORY = 135 SdkConstants.FD_ASSETS + AndroidConstants.WS_SEP; 136 private static final String DRAWABLE_DIRECTORY = 137 SdkConstants.FD_DRAWABLE + AndroidConstants.WS_SEP; 138 private static final String DRAWABLE_HDPI_DIRECTORY = 139 SdkConstants.FD_DRAWABLE + "-" + Density.HIGH.getResourceValue() + AndroidConstants.WS_SEP; //$NON-NLS-1$ 140 private static final String DRAWABLE_MDPI_DIRECTORY = 141 SdkConstants.FD_DRAWABLE + "-" + Density.MEDIUM.getResourceValue() + AndroidConstants.WS_SEP; //$NON-NLS-1$ 142 private static final String DRAWABLE_LDPI_DIRECTORY = 143 SdkConstants.FD_DRAWABLE + "-" + Density.LOW.getResourceValue() + AndroidConstants.WS_SEP; //$NON-NLS-1$ 144 private static final String LAYOUT_DIRECTORY = 145 SdkConstants.FD_LAYOUT + AndroidConstants.WS_SEP; 146 private static final String VALUES_DIRECTORY = 147 SdkConstants.FD_VALUES + AndroidConstants.WS_SEP; 148 private static final String GEN_SRC_DIRECTORY = 149 SdkConstants.FD_GEN_SOURCES + AndroidConstants.WS_SEP; 150 151 private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$ 152 private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY 153 + "AndroidManifest.template"; //$NON-NLS-1$ 154 private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY 155 + "activity.template"; //$NON-NLS-1$ 156 private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY 157 + "uses-sdk.template"; //$NON-NLS-1$ 158 private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY 159 + "launcher_intent_filter.template"; //$NON-NLS-1$ 160 private static final String TEMPLATE_TEST_USES_LIBRARY = TEMPLATES_DIRECTORY 161 + "test_uses-library.template"; //$NON-NLS-1$ 162 private static final String TEMPLATE_TEST_INSTRUMENTATION = TEMPLATES_DIRECTORY 163 + "test_instrumentation.template"; //$NON-NLS-1$ 164 165 166 167 private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY 168 + "strings.template"; //$NON-NLS-1$ 169 private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY 170 + "string.template"; //$NON-NLS-1$ 171 private static final String PROJECT_ICON = "icon.png"; //$NON-NLS-1$ 172 private static final String ICON_HDPI = "icon_hdpi.png"; //$NON-NLS-1$ 173 private static final String ICON_MDPI = "icon_mdpi.png"; //$NON-NLS-1$ 174 private static final String ICON_LDPI = "icon_ldpi.png"; //$NON-NLS-1$ 175 176 private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$ 177 178 private static final String STRING_RSRC_PREFIX = "@string/"; //$NON-NLS-1$ 179 private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$ 180 private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$ 181 182 private static final String[] DEFAULT_DIRECTORIES = new String[] { 183 BIN_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY }; 184 private static final String[] RES_DIRECTORIES = new String[] { 185 DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY }; 186 private static final String[] RES_DENSITY_ENABLED_DIRECTORIES = new String[] { 187 DRAWABLE_HDPI_DIRECTORY, DRAWABLE_MDPI_DIRECTORY, DRAWABLE_LDPI_DIRECTORY, 188 LAYOUT_DIRECTORY, VALUES_DIRECTORY }; 189 190 private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$ 191 private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$ 192 private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$ 193 private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$ 194 195 private NewProjectCreationPage mMainPage; 196 private NewTestProjectCreationPage mTestPage; 197 /** Package name available when the wizard completes. */ 198 private String mPackageName; 199 private final AvailablePages mAvailablePages; 200 201 public NewProjectWizard() { 202 this(AvailablePages.ANDROID_AND_TEST_PROJECT); 203 } 204 205 protected NewProjectWizard(AvailablePages availablePages) { 206 mAvailablePages = availablePages; 207 } 208 209 /** 210 * Initializes this creation wizard using the passed workbench and object 211 * selection. Inherited from org.eclipse.ui.IWorkbenchWizard 212 */ 213 public void init(IWorkbench workbench, IStructuredSelection selection) { 214 setHelpAvailable(false); // TODO have help 215 setImageDescriptor(); 216 217 if (mAvailablePages == AvailablePages.ANDROID_AND_TEST_PROJECT) { 218 mMainPage = createMainPage(); 219 setWindowTitle("New Android Project"); 220 } else { 221 setWindowTitle("New Android Test Project"); 222 } 223 mTestPage = createTestPage(); 224 } 225 226 /** 227 * Creates the main wizard page. 228 * <p/> 229 * Please do NOT override this method. 230 * <p/> 231 * This is protected so that it can be overridden by unit tests. 232 * However the contract of this class is private and NO ATTEMPT will be made 233 * to maintain compatibility between different versions of the plugin. 234 */ 235 protected NewProjectCreationPage createMainPage() { 236 return new NewProjectCreationPage(); 237 } 238 239 /** 240 * Creates the test wizard page. 241 * <p/> 242 * Please do NOT override this method. 243 * <p/> 244 * This is protected so that it can be overridden by unit tests. 245 * However the contract of this class is private and NO ATTEMPT will be made 246 * to maintain compatibility between different versions of the plugin. 247 */ 248 protected NewTestProjectCreationPage createTestPage() { 249 return new NewTestProjectCreationPage(); 250 } 251 252 // -- Methods inherited from org.eclipse.jface.wizard.Wizard -- 253 // The Wizard class implements most defaults and boilerplate code needed by 254 // IWizard 255 256 /** 257 * Adds pages to this wizard. 258 */ 259 @Override 260 public void addPages() { 261 if (mAvailablePages == AvailablePages.ANDROID_AND_TEST_PROJECT) { 262 addPage(mMainPage); 263 } 264 addPage(mTestPage); 265 266 if (mMainPage != null && mTestPage != null) { 267 mTestPage.setMainInfo(mMainPage.getMainInfo()); 268 mMainPage.setTestInfo(mTestPage.getTestInfo()); 269 } 270 } 271 272 /** 273 * Performs any actions appropriate in response to the user having pressed 274 * the Finish button, or refuse if finishing now is not permitted: here, it 275 * actually creates the workspace project and then switch to the Java 276 * perspective. 277 * 278 * @return True 279 */ 280 @Override 281 public boolean performFinish() { 282 if (!createAndroidProjects()) { 283 return false; 284 } 285 286 // Open the default Java Perspective 287 OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); 288 action.run(); 289 return true; 290 } 291 292 // -- Public Fields -- 293 294 /** Returns the main project package name. Only valid once the wizard finishes. */ 295 public String getPackageName() { 296 return mPackageName; 297 } 298 299 // -- Custom Methods -- 300 301 /** 302 * Before actually creating the project for a new project (as opposed to using an 303 * existing project), we check if the target location is a directory that either does 304 * not exist or is empty. 305 * 306 * If it's not empty, ask the user for confirmation. 307 * 308 * @param destination The destination folder where the new project is to be created. 309 * @return True if the destination doesn't exist yet or is an empty directory or is 310 * accepted by the user. 311 */ 312 private boolean validateNewProjectLocationIsEmpty(IPath destination) { 313 File f = new File(destination.toOSString()); 314 if (f.isDirectory() && f.list().length > 0) { 315 return AdtPlugin.displayPrompt("New Android Project", 316 "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?"); 317 } 318 return true; 319 } 320 321 /** 322 * Structure that describes all the information needed to create a project. 323 * This is collected from the pages by {@link NewProjectWizard#createAndroidProjects()} 324 * and then used by 325 * {@link NewProjectWizard#createProjectAsync(IProgressMonitor, ProjectInfo, ProjectInfo)}. 326 */ 327 private static class ProjectInfo { 328 private final IProject mProject; 329 private final IProjectDescription mDescription; 330 private final Map<String, Object> mParameters; 331 private final HashMap<String, String> mDictionary; 332 333 public ProjectInfo(IProject project, 334 IProjectDescription description, 335 Map<String, Object> parameters, 336 HashMap<String, String> dictionary) { 337 mProject = project; 338 mDescription = description; 339 mParameters = parameters; 340 mDictionary = dictionary; 341 } 342 343 public IProject getProject() { 344 return mProject; 345 } 346 347 public IProjectDescription getDescription() { 348 return mDescription; 349 } 350 351 public Map<String, Object> getParameters() { 352 return mParameters; 353 } 354 355 public HashMap<String, String> getDictionary() { 356 return mDictionary; 357 } 358 } 359 360 /** 361 * Creates the android project. 362 * @return True if the project could be created. 363 */ 364 private boolean createAndroidProjects() { 365 366 final ProjectInfo mainData = collectMainPageInfo(); 367 if (mMainPage != null && mainData == null) { 368 return false; 369 } 370 371 final ProjectInfo testData = collectTestPageInfo(); 372 373 // Create a monitored operation to create the actual project 374 WorkspaceModifyOperation op = new WorkspaceModifyOperation() { 375 @Override 376 protected void execute(IProgressMonitor monitor) throws InvocationTargetException { 377 createProjectAsync(monitor, mainData, testData); 378 } 379 }; 380 381 // Run the operation in a different thread 382 runAsyncOperation(op); 383 return true; 384 } 385 386 /** 387 * Collects all the parameters needed to create the main project. 388 * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be 389 * created because parameters are incorrect or should not be created because there 390 * is no main page. 391 */ 392 private ProjectInfo collectMainPageInfo() { 393 if (mMainPage == null) { 394 return null; 395 } 396 397 IMainInfo info = mMainPage.getMainInfo(); 398 399 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 400 final IProject project = workspace.getRoot().getProject(info.getProjectName()); 401 final IProjectDescription description = workspace.newProjectDescription(project.getName()); 402 403 // keep some variables to make them available once the wizard closes 404 mPackageName = info.getPackageName(); 405 406 final Map<String, Object> parameters = new HashMap<String, Object>(); 407 parameters.put(PARAM_PROJECT, info.getProjectName()); 408 parameters.put(PARAM_PACKAGE, mPackageName); 409 parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); 410 parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); 411 parameters.put(PARAM_IS_NEW_PROJECT, info.isNewProject()); 412 parameters.put(PARAM_SRC_FOLDER, info.getSourceFolder()); 413 parameters.put(PARAM_SDK_TARGET, info.getSdkTarget()); 414 parameters.put(PARAM_MIN_SDK_VERSION, info.getMinSdkVersion()); 415 416 if (info.isCreateActivity()) { 417 // An activity name can be of the form ".package.Class" or ".Class". 418 // The initial dot is ignored, as it is always added later in the templates. 419 String activityName = info.getActivityName(); 420 if (activityName.startsWith(".")) { //$NON-NLS-1$ 421 activityName = activityName.substring(1); 422 } 423 parameters.put(PARAM_ACTIVITY, activityName); 424 } 425 426 // create a dictionary of string that will contain name+content. 427 // we'll put all the strings into values/strings.xml 428 final HashMap<String, String> dictionary = new HashMap<String, String>(); 429 dictionary.put(STRING_APP_NAME, info.getApplicationName()); 430 431 IPath path = info.getLocationPath(); 432 IPath defaultLocation = Platform.getLocation(); 433 if (!path.equals(defaultLocation)) { 434 description.setLocation(path); 435 } 436 437 if (info.isNewProject() && !info.useDefaultLocation() && 438 !validateNewProjectLocationIsEmpty(path)) { 439 return null; 440 } 441 442 return new ProjectInfo(project, description, parameters, dictionary); 443 } 444 445 /** 446 * Collects all the parameters needed to create the test project. 447 * 448 * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be 449 * created because parameters are incorrect or should not be created because there 450 * is no test page. 451 */ 452 private ProjectInfo collectTestPageInfo() { 453 if (mTestPage == null) { 454 return null; 455 } 456 TestInfo info = mTestPage.getTestInfo(); 457 458 if (!info.getCreateTestProject()) { 459 return null; 460 } 461 462 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 463 final IProject project = workspace.getRoot().getProject(info.getProjectName()); 464 final IProjectDescription description = workspace.newProjectDescription(project.getName()); 465 466 final Map<String, Object> parameters = new HashMap<String, Object>(); 467 parameters.put(PARAM_PROJECT, info.getProjectName()); 468 parameters.put(PARAM_PACKAGE, info.getPackageName()); 469 parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); 470 parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); 471 parameters.put(PARAM_IS_NEW_PROJECT, true); 472 parameters.put(PARAM_SRC_FOLDER, info.getSourceFolder()); 473 parameters.put(PARAM_SDK_TARGET, info.getSdkTarget()); 474 parameters.put(PARAM_MIN_SDK_VERSION, info.getMinSdkVersion()); 475 476 // Test-specific parameters 477 parameters.put(PARAM_TEST_TARGET_PACKAGE, info.getTargetPackageName()); 478 479 if (info.isTestingSelf()) { 480 parameters.put(PARAM_TARGET_SELF, true); 481 } 482 if (info.isTestingMain()) { 483 parameters.put(PARAM_TARGET_MAIN, true); 484 } 485 if (info.isTestingExisting()) { 486 parameters.put(PARAM_TARGET_EXISTING, true); 487 parameters.put(PARAM_REFERENCE_PROJECT, info.getExistingTestedProject()); 488 } 489 490 // create a dictionary of string that will contain name+content. 491 // we'll put all the strings into values/strings.xml 492 final HashMap<String, String> dictionary = new HashMap<String, String>(); 493 dictionary.put(STRING_APP_NAME, info.getApplicationName()); 494 495 IPath path = info.getLocationPath(); 496 IPath defaultLocation = Platform.getLocation(); 497 if (!path.equals(defaultLocation)) { 498 description.setLocation(path); 499 } 500 501 if (!info.useDefaultLocation() && !validateNewProjectLocationIsEmpty(path)) { 502 return null; 503 } 504 505 return new ProjectInfo(project, description, parameters, dictionary); 506 } 507 508 /** 509 * Runs the operation in a different thread and display generated 510 * exceptions. 511 * 512 * @param op The asynchronous operation to run. 513 */ 514 private void runAsyncOperation(WorkspaceModifyOperation op) { 515 try { 516 getContainer().run(true /* fork */, true /* cancelable */, op); 517 } catch (InvocationTargetException e) { 518 519 AdtPlugin.log(e, "New Project Wizard failed"); 520 521 // The runnable threw an exception 522 Throwable t = e.getTargetException(); 523 if (t instanceof CoreException) { 524 CoreException core = (CoreException) t; 525 if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { 526 // The error indicates the file system is not case sensitive 527 // and there's a resource with a similar name. 528 MessageDialog.openError(getShell(), "Error", "Error: Case Variant Exists"); 529 } else { 530 ErrorDialog.openError(getShell(), "Error", core.getMessage(), core.getStatus()); 531 } 532 } else { 533 // Some other kind of exception 534 String msg = t.getMessage(); 535 Throwable t1 = t; 536 while (msg == null && t1.getCause() != null) { 537 msg = t1.getMessage(); 538 t1 = t1.getCause(); 539 } 540 if (msg == null) { 541 msg = t.toString(); 542 } 543 MessageDialog.openError(getShell(), "Error", msg); 544 } 545 e.printStackTrace(); 546 } catch (InterruptedException e) { 547 e.printStackTrace(); 548 } 549 } 550 551 /** 552 * Creates the actual project(s). This is run asynchronously in a different thread. 553 * 554 * @param monitor An existing monitor. 555 * @param mainData Data for main project. Can be null. 556 * @throws InvocationTargetException to wrap any unmanaged exception and 557 * return it to the calling thread. The method can fail if it fails 558 * to create or modify the project or if it is canceled by the user. 559 */ 560 private void createProjectAsync(IProgressMonitor monitor, 561 ProjectInfo mainData, 562 ProjectInfo testData) 563 throws InvocationTargetException { 564 monitor.beginTask("Create Android Project", 100); 565 try { 566 IProject mainProject = null; 567 568 if (mainData != null) { 569 mainProject = createEclipseProject( 570 new SubProgressMonitor(monitor, 50), 571 mainData.getProject(), 572 mainData.getDescription(), 573 mainData.getParameters(), 574 mainData.getDictionary()); 575 } 576 577 if (testData != null) { 578 579 Map<String, Object> parameters = testData.getParameters(); 580 if (parameters.containsKey(PARAM_TARGET_MAIN) && mainProject != null) { 581 parameters.put(PARAM_REFERENCE_PROJECT, mainProject); 582 } 583 584 createEclipseProject( 585 new SubProgressMonitor(monitor, 50), 586 testData.getProject(), 587 testData.getDescription(), 588 parameters, 589 testData.getDictionary()); 590 } 591 592 } catch (CoreException e) { 593 throw new InvocationTargetException(e); 594 } catch (IOException e) { 595 throw new InvocationTargetException(e); 596 } catch (StreamException e) { 597 throw new InvocationTargetException(e); 598 } finally { 599 monitor.done(); 600 } 601 } 602 603 /** 604 * Creates the actual project, sets its nature and adds the required folders 605 * and files to it. This is run asynchronously in a different thread. 606 * 607 * @param monitor An existing monitor. 608 * @param project The project to create. 609 * @param description A description of the project. 610 * @param parameters Template parameters. 611 * @param dictionary String definition. 612 * @return The project newly created 613 * @throws StreamException 614 */ 615 private IProject createEclipseProject(IProgressMonitor monitor, 616 IProject project, 617 IProjectDescription description, 618 Map<String, Object> parameters, 619 Map<String, String> dictionary) 620 throws CoreException, IOException, StreamException { 621 622 // get the project target 623 IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET); 624 boolean legacy = target.getVersion().getApiLevel() < 4; 625 626 // Create project and open it 627 project.create(description, new SubProgressMonitor(monitor, 10)); 628 if (monitor.isCanceled()) throw new OperationCanceledException(); 629 630 project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10)); 631 632 // Add the Java and android nature to the project 633 AndroidNature.setupProjectNatures(project, monitor); 634 635 // Create folders in the project if they don't already exist 636 addDefaultDirectories(project, AndroidConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); 637 String[] sourceFolders = new String[] { 638 (String) parameters.get(PARAM_SRC_FOLDER), 639 GEN_SRC_DIRECTORY 640 }; 641 addDefaultDirectories(project, AndroidConstants.WS_ROOT, sourceFolders, monitor); 642 643 // Create the resource folders in the project if they don't already exist. 644 if (legacy) { 645 addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor); 646 } else { 647 addDefaultDirectories(project, RES_DIRECTORY, RES_DENSITY_ENABLED_DIRECTORIES, monitor); 648 } 649 650 // Setup class path: mark folders as source folders 651 IJavaProject javaProject = JavaCore.create(project); 652 setupSourceFolders(javaProject, sourceFolders, monitor); 653 654 // Mark the gen source folder as derived 655 IFolder genSrcFolder = project.getFolder(AndroidConstants.WS_ROOT + GEN_SRC_DIRECTORY); 656 if (genSrcFolder.exists()) { 657 genSrcFolder.setDerived(true); 658 } 659 660 if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) { 661 // Create files in the project if they don't already exist 662 addManifest(project, parameters, dictionary, monitor); 663 664 // add the default app icon 665 addIcon(project, legacy, monitor); 666 667 // Create the default package components 668 addSampleCode(project, sourceFolders[0], parameters, dictionary, monitor); 669 670 // add the string definition file if needed 671 if (dictionary.size() > 0) { 672 addStringDictionaryFile(project, dictionary, monitor); 673 } 674 675 // Set output location 676 javaProject.setOutputLocation(project.getFolder(BIN_DIRECTORY).getFullPath(), 677 monitor); 678 } 679 680 // Create the reference to the target project 681 if (parameters.containsKey(PARAM_REFERENCE_PROJECT)) { 682 IProject refProject = (IProject) parameters.get(PARAM_REFERENCE_PROJECT); 683 if (refProject != null) { 684 IProjectDescription desc = project.getDescription(); 685 686 // Add out reference to the existing project reference. 687 // We just created a project with no references so we don't need to expand 688 // the currently-empty current list. 689 desc.setReferencedProjects(new IProject[] { refProject }); 690 691 project.setDescription(desc, IResource.KEEP_HISTORY, 692 new SubProgressMonitor(monitor, 10)); 693 694 IClasspathEntry entry = JavaCore.newProjectEntry( 695 refProject.getFullPath(), //path 696 new IAccessRule[0], //accessRules 697 false, //combineAccessRules 698 new IClasspathAttribute[0], //extraAttributes 699 false //isExported 700 701 ); 702 ProjectHelper.addEntryToClasspath(javaProject, entry); 703 } 704 } 705 706 Sdk.getCurrent().initProject(project, target); 707 708 // Fix the project to make sure all properties are as expected. 709 // Necessary for existing projects and good for new ones to. 710 ProjectHelper.fixProject(project); 711 712 return project; 713 } 714 715 /** 716 * Adds default directories to the project. 717 * 718 * @param project The Java Project to update. 719 * @param parentFolder The path of the parent folder. Must end with a 720 * separator. 721 * @param folders Folders to be added. 722 * @param monitor An existing monitor. 723 * @throws CoreException if the method fails to create the directories in 724 * the project. 725 */ 726 private void addDefaultDirectories(IProject project, String parentFolder, 727 String[] folders, IProgressMonitor monitor) throws CoreException { 728 for (String name : folders) { 729 if (name.length() > 0) { 730 IFolder folder = project.getFolder(parentFolder + name); 731 if (!folder.exists()) { 732 folder.create(true /* force */, true /* local */, 733 new SubProgressMonitor(monitor, 10)); 734 } 735 } 736 } 737 } 738 739 /** 740 * Adds the manifest to the project. 741 * 742 * @param project The Java Project to update. 743 * @param parameters Template Parameters. 744 * @param dictionary String List to be added to a string definition 745 * file. This map will be filled by this method. 746 * @param monitor An existing monitor. 747 * @throws CoreException if the method fails to update the project. 748 * @throws IOException if the method fails to create the files in the 749 * project. 750 */ 751 private void addManifest(IProject project, Map<String, Object> parameters, 752 Map<String, String> dictionary, IProgressMonitor monitor) 753 throws CoreException, IOException { 754 755 // get IFile to the manifest and check if it's not already there. 756 IFile file = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); 757 if (!file.exists()) { 758 759 // Read manifest template 760 String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST); 761 762 // Replace all keyword parameters 763 manifestTemplate = replaceParameters(manifestTemplate, parameters); 764 765 if (manifestTemplate == null) { 766 // Inform the user there will be not manifest. 767 AdtPlugin.logAndPrintError(null, getWindowTitle() /*TAG*/, 768 "Failed to generate the Android manifest. Missing template %s", 769 TEMPLATE_MANIFEST); 770 // Abort now, there's no need to continue 771 return; 772 } 773 774 if (parameters.containsKey(PARAM_ACTIVITY)) { 775 // now get the activity template 776 String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES); 777 778 // Replace all keyword parameters to make main activity. 779 String activities = replaceParameters(activityTemplate, parameters); 780 781 // set the intent. 782 String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER); 783 784 if (activities != null) { 785 if (intent != null) { 786 // set the intent to the main activity 787 activities = activities.replaceAll(PH_INTENT_FILTERS, intent); 788 } 789 790 // set the activity(ies) in the manifest 791 manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities); 792 } 793 } else { 794 // remove the activity(ies) from the manifest 795 manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, ""); //$NON-NLS-1$ 796 } 797 798 // Handle the case of the test projects 799 if (parameters.containsKey(PARAM_TEST_TARGET_PACKAGE)) { 800 // Set the uses-library needed by the test project 801 String usesLibrary = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_USES_LIBRARY); 802 if (usesLibrary != null) { 803 manifestTemplate = manifestTemplate.replaceAll( 804 PH_TEST_USES_LIBRARY, usesLibrary); 805 } 806 807 // Set the instrumentation element needed by the test project 808 String instru = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_INSTRUMENTATION); 809 if (instru != null) { 810 manifestTemplate = manifestTemplate.replaceAll( 811 PH_TEST_INSTRUMENTATION, instru); 812 } 813 814 // Replace PARAM_TEST_TARGET_PACKAGE itself now 815 manifestTemplate = replaceParameters(manifestTemplate, parameters); 816 817 } else { 818 // remove the unused entries 819 manifestTemplate = manifestTemplate.replaceAll(PH_TEST_USES_LIBRARY, ""); //$NON-NLS-1$ 820 manifestTemplate = manifestTemplate.replaceAll(PH_TEST_INSTRUMENTATION, ""); //$NON-NLS-1$ 821 } 822 823 String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION); 824 if (minSdkVersion != null && minSdkVersion.length() > 0) { 825 String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK); 826 if (usesSdkTemplate != null) { 827 String usesSdk = replaceParameters(usesSdkTemplate, parameters); 828 manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk); 829 } 830 } else { 831 manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, ""); 832 } 833 834 // Save in the project as UTF-8 835 InputStream stream = new ByteArrayInputStream( 836 manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$ 837 file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); 838 } 839 } 840 841 /** 842 * Adds the string resource file. 843 * 844 * @param project The Java Project to update. 845 * @param strings The list of strings to be added to the string file. 846 * @param monitor An existing monitor. 847 * @throws CoreException if the method fails to update the project. 848 * @throws IOException if the method fails to create the files in the 849 * project. 850 */ 851 private void addStringDictionaryFile(IProject project, 852 Map<String, String> strings, IProgressMonitor monitor) 853 throws CoreException, IOException { 854 855 // create the IFile object and check if the file doesn't already exist. 856 IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP 857 + VALUES_DIRECTORY + AndroidConstants.WS_SEP + STRINGS_FILE); 858 if (!file.exists()) { 859 // get the Strings.xml template 860 String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS); 861 862 // get the template for one string 863 String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING); 864 865 // get all the string names 866 Set<String> stringNames = strings.keySet(); 867 868 // loop on it and create the string definitions 869 StringBuilder stringNodes = new StringBuilder(); 870 for (String key : stringNames) { 871 // get the value from the key 872 String value = strings.get(key); 873 874 // place them in the template 875 String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key); 876 stringDef = stringDef.replace(PARAM_STRING_CONTENT, value); 877 878 // append to the other string 879 if (stringNodes.length() > 0) { 880 stringNodes.append("\n"); 881 } 882 stringNodes.append(stringDef); 883 } 884 885 // put the string nodes in the Strings.xml template 886 stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS, 887 stringNodes.toString()); 888 889 // write the file as UTF-8 890 InputStream stream = new ByteArrayInputStream( 891 stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$ 892 file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); 893 } 894 } 895 896 897 /** 898 * Adds default application icon to the project. 899 * 900 * @param project The Java Project to update. 901 * @param legacy whether we're running in legacy mode (no density support) 902 * @param monitor An existing monitor. 903 * @throws CoreException if the method fails to update the project. 904 */ 905 private void addIcon(IProject project, boolean legacy, IProgressMonitor monitor) 906 throws CoreException { 907 if (legacy) { // density support 908 // do medium density icon only, in the default drawable folder. 909 IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP 910 + DRAWABLE_DIRECTORY + AndroidConstants.WS_SEP + PROJECT_ICON); 911 if (!file.exists()) { 912 addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); 913 } 914 } else { 915 // do all 3 icons. 916 IFile file; 917 918 // high density 919 file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP 920 + DRAWABLE_HDPI_DIRECTORY + AndroidConstants.WS_SEP + PROJECT_ICON); 921 if (!file.exists()) { 922 addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_HDPI), monitor); 923 } 924 925 // medium density 926 file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP 927 + DRAWABLE_MDPI_DIRECTORY + AndroidConstants.WS_SEP + PROJECT_ICON); 928 if (!file.exists()) { 929 addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); 930 } 931 932 // low density 933 file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP 934 + DRAWABLE_LDPI_DIRECTORY + AndroidConstants.WS_SEP + PROJECT_ICON); 935 if (!file.exists()) { 936 addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_LDPI), monitor); 937 } 938 } 939 } 940 941 /** 942 * Creates a file from a data source. 943 * @param dest the file to write 944 * @param source the content of the file. 945 * @param monitor the progress monitor 946 * @throws CoreException 947 */ 948 private void addFile(IFile dest, byte[] source, IProgressMonitor monitor) throws CoreException { 949 if (source != null) { 950 // Save in the project 951 InputStream stream = new ByteArrayInputStream(source); 952 dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); 953 } 954 } 955 956 /** 957 * Creates the package folder and copies the sample code in the project. 958 * 959 * @param project The Java Project to update. 960 * @param parameters Template Parameters. 961 * @param dictionary String List to be added to a string definition 962 * file. This map will be filled by this method. 963 * @param monitor An existing monitor. 964 * @throws CoreException if the method fails to update the project. 965 * @throws IOException if the method fails to create the files in the 966 * project. 967 */ 968 private void addSampleCode(IProject project, String sourceFolder, 969 Map<String, Object> parameters, Map<String, String> dictionary, 970 IProgressMonitor monitor) throws CoreException, IOException { 971 // create the java package directories. 972 IFolder pkgFolder = project.getFolder(sourceFolder); 973 String packageName = (String) parameters.get(PARAM_PACKAGE); 974 975 // The PARAM_ACTIVITY key will be absent if no activity should be created, 976 // in which case activityName will be null. 977 String activityName = (String) parameters.get(PARAM_ACTIVITY); 978 Map<String, Object> java_activity_parameters = parameters; 979 if (activityName != null) { 980 if (activityName.indexOf('.') >= 0) { 981 // There are package names in the activity name. Transform packageName to add 982 // those sub packages and remove them from activityName. 983 packageName += "." + activityName; //$NON-NLS-1$ 984 int pos = packageName.lastIndexOf('.'); 985 activityName = packageName.substring(pos + 1); 986 packageName = packageName.substring(0, pos); 987 988 // Also update the values used in the JAVA_FILE_TEMPLATE below 989 // (but not the ones from the manifest so don't change the caller's dictionary) 990 java_activity_parameters = new HashMap<String, Object>(parameters); 991 java_activity_parameters.put(PARAM_PACKAGE, packageName); 992 java_activity_parameters.put(PARAM_ACTIVITY, activityName); 993 } 994 } 995 996 String[] components = packageName.split(AndroidConstants.RE_DOT); 997 for (String component : components) { 998 pkgFolder = pkgFolder.getFolder(component); 999 if (!pkgFolder.exists()) { 1000 pkgFolder.create(true /* force */, true /* local */, 1001 new SubProgressMonitor(monitor, 10)); 1002 } 1003 } 1004 1005 if (activityName != null) { 1006 // create the main activity Java file 1007 String activityJava = activityName + AndroidConstants.DOT_JAVA; 1008 IFile file = pkgFolder.getFile(activityJava); 1009 if (!file.exists()) { 1010 copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor); 1011 } 1012 } 1013 1014 // create the layout file 1015 IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY); 1016 IFile file = layoutfolder.getFile(MAIN_LAYOUT_XML); 1017 if (!file.exists()) { 1018 copyFile(LAYOUT_TEMPLATE, file, parameters, monitor); 1019 if (activityName != null) { 1020 dictionary.put(STRING_HELLO_WORLD, "Hello World, " + activityName + "!"); 1021 } else { 1022 dictionary.put(STRING_HELLO_WORLD, "Hello World!"); 1023 } 1024 } 1025 } 1026 1027 /** 1028 * Adds the given folder to the project's class path. 1029 * 1030 * @param javaProject The Java Project to update. 1031 * @param sourceFolder Template Parameters. 1032 * @param monitor An existing monitor. 1033 * @throws JavaModelException if the classpath could not be set. 1034 */ 1035 private void setupSourceFolders(IJavaProject javaProject, String[] sourceFolders, 1036 IProgressMonitor monitor) throws JavaModelException { 1037 IProject project = javaProject.getProject(); 1038 1039 // get the list of entries. 1040 IClasspathEntry[] entries = javaProject.getRawClasspath(); 1041 1042 // remove the project as a source folder (This is the default) 1043 entries = removeSourceClasspath(entries, project); 1044 1045 // add the source folders. 1046 for (String sourceFolder : sourceFolders) { 1047 IFolder srcFolder = project.getFolder(sourceFolder); 1048 1049 // remove it first in case. 1050 entries = removeSourceClasspath(entries, srcFolder); 1051 entries = ProjectHelper.addEntryToClasspath(entries, 1052 JavaCore.newSourceEntry(srcFolder.getFullPath())); 1053 } 1054 1055 javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); 1056 } 1057 1058 1059 /** 1060 * Removes the corresponding source folder from the class path entries if 1061 * found. 1062 * 1063 * @param entries The class path entries to read. A copy will be returned. 1064 * @param folder The parent source folder to remove. 1065 * @return A new class path entries array. 1066 */ 1067 private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) { 1068 if (folder == null) { 1069 return entries; 1070 } 1071 IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath()); 1072 int n = entries.length; 1073 for (int i = n - 1; i >= 0; i--) { 1074 if (entries[i].equals(source)) { 1075 IClasspathEntry[] newEntries = new IClasspathEntry[n - 1]; 1076 if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i); 1077 if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1); 1078 n--; 1079 entries = newEntries; 1080 } 1081 } 1082 return entries; 1083 } 1084 1085 1086 /** 1087 * Copies the given file from our resource folder to the new project. 1088 * Expects the file to the US-ASCII or UTF-8 encoded. 1089 * 1090 * @throws CoreException from IFile if failing to create the new file. 1091 * @throws MalformedURLException from URL if failing to interpret the URL. 1092 * @throws FileNotFoundException from RandomAccessFile. 1093 * @throws IOException from RandomAccessFile.length() if can't determine the 1094 * length. 1095 */ 1096 private void copyFile(String resourceFilename, IFile destFile, 1097 Map<String, Object> parameters, IProgressMonitor monitor) 1098 throws CoreException, IOException { 1099 1100 // Read existing file. 1101 String template = AdtPlugin.readEmbeddedTextFile( 1102 TEMPLATES_DIRECTORY + resourceFilename); 1103 1104 // Replace all keyword parameters 1105 template = replaceParameters(template, parameters); 1106 1107 // Save in the project as UTF-8 1108 InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$ 1109 destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); 1110 } 1111 1112 /** 1113 * Returns an image descriptor for the wizard logo. 1114 */ 1115 private void setImageDescriptor() { 1116 ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); 1117 setDefaultPageImageDescriptor(desc); 1118 } 1119 1120 /** 1121 * Replaces placeholders found in a string with values. 1122 * 1123 * @param str the string to search for placeholders. 1124 * @param parameters a map of <placeholder, Value> to search for in the string 1125 * @return A new String object with the placeholder replaced by the values. 1126 */ 1127 private String replaceParameters(String str, Map<String, Object> parameters) { 1128 1129 if (parameters == null) { 1130 AdtPlugin.log(IStatus.ERROR, 1131 "NPW replace parameters: null parameter map. String: '%s'", str); //$NON-NLS-1$ 1132 return str; 1133 } else if (str == null) { 1134 AdtPlugin.log(IStatus.ERROR, 1135 "NPW replace parameters: null template string"); //$NON-NLS-1$ 1136 return str; 1137 } 1138 1139 for (Entry<String, Object> entry : parameters.entrySet()) { 1140 if (entry != null && entry.getValue() instanceof String) { 1141 Object value = entry.getValue(); 1142 if (value == null) { 1143 AdtPlugin.log(IStatus.ERROR, 1144 "NPW replace parameters: null value for key '%s' in template '%s'", //$NON-NLS-1$ 1145 entry.getKey(), 1146 str); 1147 } else { 1148 str = str.replaceAll(entry.getKey(), (String) value); 1149 } 1150 } 1151 } 1152 1153 return str; 1154 } 1155 } 1156