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 /* 18 * References: 19 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard 20 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage 21 */ 22 23 package com.android.ide.eclipse.adt.internal.wizards.newproject; 24 25 import com.android.ide.eclipse.adt.AdtPlugin; 26 import com.android.ide.eclipse.adt.AndroidConstants; 27 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 28 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 29 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 30 import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectCreationPage.TestInfo; 31 import com.android.sdklib.IAndroidTarget; 32 import com.android.sdklib.SdkConstants; 33 import com.android.sdklib.internal.project.ProjectProperties; 34 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; 35 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 36 import com.android.sdklib.xml.AndroidManifest; 37 import com.android.sdklib.xml.ManifestData; 38 import com.android.sdklib.xml.ManifestData.Activity; 39 import com.android.sdkuilib.internal.widgets.SdkTargetSelector; 40 41 import org.eclipse.core.filesystem.URIUtil; 42 import org.eclipse.core.resources.IProject; 43 import org.eclipse.core.resources.IResource; 44 import org.eclipse.core.resources.IWorkspace; 45 import org.eclipse.core.resources.ResourcesPlugin; 46 import org.eclipse.core.runtime.IPath; 47 import org.eclipse.core.runtime.IStatus; 48 import org.eclipse.core.runtime.Path; 49 import org.eclipse.core.runtime.Platform; 50 import org.eclipse.jdt.core.JavaConventions; 51 import org.eclipse.jface.wizard.WizardPage; 52 import org.eclipse.osgi.util.TextProcessor; 53 import org.eclipse.swt.SWT; 54 import org.eclipse.swt.custom.ScrolledComposite; 55 import org.eclipse.swt.events.ControlAdapter; 56 import org.eclipse.swt.events.ControlEvent; 57 import org.eclipse.swt.events.SelectionAdapter; 58 import org.eclipse.swt.events.SelectionEvent; 59 import org.eclipse.swt.events.SelectionListener; 60 import org.eclipse.swt.graphics.Rectangle; 61 import org.eclipse.swt.layout.GridData; 62 import org.eclipse.swt.layout.GridLayout; 63 import org.eclipse.swt.widgets.Button; 64 import org.eclipse.swt.widgets.Combo; 65 import org.eclipse.swt.widgets.Composite; 66 import org.eclipse.swt.widgets.DirectoryDialog; 67 import org.eclipse.swt.widgets.Event; 68 import org.eclipse.swt.widgets.Group; 69 import org.eclipse.swt.widgets.Label; 70 import org.eclipse.swt.widgets.Listener; 71 import org.eclipse.swt.widgets.Text; 72 73 import java.io.File; 74 import java.io.FileFilter; 75 import java.net.URI; 76 import java.util.ArrayList; 77 import java.util.regex.Pattern; 78 79 /** 80 * NewAndroidProjectCreationPage is a project creation page that provides the 81 * following fields: 82 * <ul> 83 * <li> Project name 84 * <li> SDK Target 85 * <li> Application name 86 * <li> Package name 87 * <li> Activity name 88 * </ul> 89 * Note: this class is public so that it can be accessed from unit tests. 90 * It is however an internal class. Its API may change without notice. 91 * It should semantically be considered as a private final class. 92 * Do not derive from this class. 93 */ 94 public class NewProjectCreationPage extends WizardPage { 95 96 // constants 97 private static final String MAIN_PAGE_NAME = "newAndroidProjectPage"; //$NON-NLS-1$ 98 99 /** Initial value for all name fields (project, activity, application, package). Used 100 * whenever a value is requested before controls are created. */ 101 private static final String INITIAL_NAME = ""; //$NON-NLS-1$ 102 /** Initial value for the Create New Project radio. */ 103 private static final boolean INITIAL_CREATE_NEW_PROJECT = true; 104 /** Initial value for the Create Project From Sample. */ 105 private static final boolean INITIAL_CREATE_FROM_SAMPLE = false; 106 /** Initial value for the Create Project From Existing Source. */ 107 private static final boolean INITIAL_CREATE_FROM_SOURCE = false; 108 /** Initial value for the Use Default Location check box. */ 109 private static final boolean INITIAL_USE_DEFAULT_LOCATION = true; 110 /** Initial value for the Create Activity check box. */ 111 private static final boolean INITIAL_CREATE_ACTIVITY = true; 112 113 114 /** Pattern for characters accepted in a project name. Since this will be used as a 115 * directory name, we're being a bit conservative on purpose. It cannot start with a space. */ 116 private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$ 117 /** Last user-browsed location, static so that it be remembered for the whole session */ 118 private static String sCustomLocationOsPath = ""; //$NON-NLS-1$ 119 private static boolean sAutoComputeCustomLocation = true; 120 121 private final int MSG_NONE = 0; 122 private final int MSG_WARNING = 1; 123 private final int MSG_ERROR = 2; 124 125 /** Structure with the externally visible information from this Main Project page. */ 126 private final MainInfo mInfo = new MainInfo(); 127 /** Structure with the externally visible information from the Test Project page. 128 * This is null if there's no such page, meaning the main project page is standalone. */ 129 private TestInfo mTestInfo; 130 131 private String mUserPackageName = ""; //$NON-NLS-1$ 132 private String mUserActivityName = ""; //$NON-NLS-1$ 133 private boolean mUserCreateActivityCheck = INITIAL_CREATE_ACTIVITY; 134 private String mSourceFolder = ""; //$NON-NLS-1$ 135 136 // widgets 137 private Text mProjectNameField; 138 private Text mPackageNameField; 139 private Text mActivityNameField; 140 private Text mApplicationNameField; 141 private Button mCreateNewProjectRadio; 142 private Button mCreateFromSampleRadio; 143 private Button mUseDefaultLocation; 144 private Label mLocationLabel; 145 private Text mLocationPathField; 146 private Button mBrowseButton; 147 private Button mCreateActivityCheck; 148 private Text mMinSdkVersionField; 149 private SdkTargetSelector mSdkTargetSelector; 150 private ITargetChangeListener mSdkTargetChangeListener; 151 152 private boolean mInternalLocationPathUpdate; 153 private boolean mInternalProjectNameUpdate; 154 private boolean mInternalApplicationNameUpdate; 155 private boolean mInternalCreateActivityUpdate; 156 private boolean mInternalActivityNameUpdate; 157 private boolean mProjectNameModifiedByUser; 158 private boolean mApplicationNameModifiedByUser; 159 160 private final ArrayList<String> mSamplesPaths = new ArrayList<String>(); 161 private Combo mSamplesCombo; 162 163 164 165 /** 166 * Creates a new project creation wizard page. 167 */ 168 public NewProjectCreationPage() { 169 super(MAIN_PAGE_NAME); 170 setPageComplete(false); 171 setTitle("New Android Project"); 172 setDescription("Creates a new Android Project resource."); 173 } 174 175 // --- Getters used by NewProjectWizard --- 176 177 178 /** 179 * Structure that collects all externally visible information from this page. 180 * This is used by the calling wizard to actually do the work or by other pages. 181 * <p/> 182 * This interface is provided so that the adt-test counterpart can override the returned 183 * information. 184 */ 185 public interface IMainInfo { 186 public IPath getLocationPath(); 187 /** 188 * Returns the current project location path as entered by the user, or its 189 * anticipated initial value. Note that if the default has been returned the 190 * path in a project description used to create a project should not be set. 191 * 192 * @return the project location path or its anticipated initial value. 193 */ 194 /** Returns the value of the project name field with leading and trailing spaces removed. */ 195 public String getProjectName(); 196 /** Returns the value of the package name field with spaces trimmed. */ 197 public String getPackageName(); 198 /** Returns the value of the activity name field with spaces trimmed. */ 199 public String getActivityName(); 200 /** Returns the value of the min sdk version field with spaces trimmed. */ 201 public String getMinSdkVersion(); 202 /** Returns the value of the application name field with spaces trimmed. */ 203 public String getApplicationName(); 204 /** Returns the value of the "Create New Project" radio. */ 205 public boolean isNewProject(); 206 /** Returns the value of the "Create Activity" checkbox. */ 207 public boolean isCreateActivity(); 208 /** Returns the value of the Use Default Location field. */ 209 public boolean useDefaultLocation(); 210 /** Returns the internal source folder (for the "existing project" mode) or the default 211 * "src" constant. */ 212 public String getSourceFolder(); 213 /** Returns the current sdk target or null if none has been selected yet. */ 214 public IAndroidTarget getSdkTarget(); 215 } 216 217 218 /** 219 * Structure that collects all externally visible information from this page. 220 * This is used by the calling wizard to actually do the work or by other pages. 221 */ 222 public class MainInfo implements IMainInfo { 223 /** 224 * Returns the current project location path as entered by the user, or its 225 * anticipated initial value. Note that if the default has been returned the 226 * path in a project description used to create a project should not be set. 227 * 228 * @return the project location path or its anticipated initial value. 229 */ 230 public IPath getLocationPath() { 231 return new Path(getProjectLocation()); 232 } 233 234 /** Returns the value of the project name field with leading and trailing spaces removed. */ 235 public String getProjectName() { 236 return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim(); 237 } 238 239 /** Returns the value of the package name field with spaces trimmed. */ 240 public String getPackageName() { 241 return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim(); 242 } 243 244 /** Returns the value of the activity name field with spaces trimmed. */ 245 public String getActivityName() { 246 return mActivityNameField == null ? INITIAL_NAME : mActivityNameField.getText().trim(); 247 } 248 249 /** Returns the value of the min sdk version field with spaces trimmed. */ 250 public String getMinSdkVersion() { 251 return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim(); //$NON-NLS-1$ 252 } 253 254 /** Returns the value of the application name field with spaces trimmed. */ 255 public String getApplicationName() { 256 // Return the name of the activity as default application name. 257 return mApplicationNameField == null ? getActivityName() 258 : mApplicationNameField.getText().trim(); 259 260 } 261 262 /** Returns the value of the "Create New Project" radio. */ 263 public boolean isNewProject() { 264 return mCreateNewProjectRadio == null ? INITIAL_CREATE_NEW_PROJECT 265 : mCreateNewProjectRadio.getSelection(); 266 } 267 268 /** Returns the value of the "Create from Existing Sample" radio. */ 269 public boolean isCreateFromSample() { 270 return mCreateFromSampleRadio == null ? INITIAL_CREATE_FROM_SAMPLE 271 : mCreateFromSampleRadio.getSelection(); 272 } 273 274 /** Returns the value of the "Create Activity" checkbox. */ 275 public boolean isCreateActivity() { 276 return mCreateActivityCheck == null ? INITIAL_CREATE_ACTIVITY 277 : mCreateActivityCheck.getSelection(); 278 } 279 280 /** Returns the value of the Use Default Location field. */ 281 public boolean useDefaultLocation() { 282 return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION 283 : mUseDefaultLocation.getSelection(); 284 } 285 286 /** Returns the internal source folder (for the "existing project" mode) or the default 287 * "src" constant. */ 288 public String getSourceFolder() { 289 if (isNewProject() || mSourceFolder == null || mSourceFolder.length() == 0) { 290 return SdkConstants.FD_SOURCES; 291 } else { 292 return mSourceFolder; 293 } 294 } 295 296 /** Returns the current sdk target or null if none has been selected yet. */ 297 public IAndroidTarget getSdkTarget() { 298 return mSdkTargetSelector == null ? null : mSdkTargetSelector.getSelected(); 299 } 300 } 301 302 /** 303 * Returns a {@link MainInfo} structure that collects all externally visible information 304 * from this page, to be used by the calling wizard or by other pages. 305 */ 306 public IMainInfo getMainInfo() { 307 return mInfo; 308 } 309 310 /** 311 * Grabs the {@link TestInfo} structure that collects externally visible fields from the 312 * test project page. This may be null. 313 */ 314 public void setTestInfo(TestInfo testInfo) { 315 mTestInfo = testInfo; 316 } 317 318 /** 319 * Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when 320 * the dialog is made visible. 321 */ 322 @Override 323 public void setVisible(boolean visible) { 324 super.setVisible(visible); 325 if (visible) { 326 mProjectNameField.setFocus(); 327 validatePageComplete(); 328 } 329 } 330 331 // --- UI creation --- 332 333 /** 334 * Creates the top level control for this dialog page under the given parent 335 * composite. 336 * 337 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) 338 */ 339 public void createControl(Composite parent) { 340 final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL); 341 scrolledComposite.setFont(parent.getFont()); 342 scrolledComposite.setExpandHorizontal(true); 343 scrolledComposite.setExpandVertical(true); 344 initializeDialogUnits(parent); 345 346 final Composite composite = new Composite(scrolledComposite, SWT.NULL); 347 composite.setFont(parent.getFont()); 348 scrolledComposite.setContent(composite); 349 350 composite.setLayout(new GridLayout()); 351 composite.setLayoutData(new GridData(GridData.FILL_BOTH)); 352 353 createProjectNameGroup(composite); 354 createLocationGroup(composite); 355 createTargetGroup(composite); 356 createPropertiesGroup(composite); 357 358 // Update state the first time 359 enableLocationWidgets(); 360 loadSamplesForTarget(null /*target*/); 361 mSdkTargetChangeListener.onSdkLoaded(); 362 363 scrolledComposite.addControlListener(new ControlAdapter() { 364 @Override 365 public void controlResized(ControlEvent e) { 366 Rectangle r = scrolledComposite.getClientArea(); 367 scrolledComposite.setMinSize(composite.computeSize(r.width, SWT.DEFAULT)); 368 } 369 }); 370 371 // Show description the first time 372 setErrorMessage(null); 373 setMessage(null); 374 setControl(scrolledComposite); 375 376 // Validate. This will complain about the first empty field. 377 validatePageComplete(); 378 } 379 380 @Override 381 public void dispose() { 382 383 if (mSdkTargetChangeListener != null) { 384 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); 385 mSdkTargetChangeListener = null; 386 } 387 388 super.dispose(); 389 } 390 391 /** 392 * Creates the group for the project name: 393 * [label: "Project Name"] [text field] 394 * 395 * @param parent the parent composite 396 */ 397 private final void createProjectNameGroup(Composite parent) { 398 Composite group = new Composite(parent, SWT.NONE); 399 GridLayout layout = new GridLayout(); 400 layout.numColumns = 2; 401 group.setLayout(layout); 402 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 403 404 // new project label 405 Label label = new Label(group, SWT.NONE); 406 label.setText("Project name:"); 407 label.setFont(parent.getFont()); 408 label.setToolTipText("Name of the Eclipse project to create. It cannot be empty."); 409 410 // new project name entry field 411 mProjectNameField = new Text(group, SWT.BORDER); 412 GridData data = new GridData(GridData.FILL_HORIZONTAL); 413 mProjectNameField.setToolTipText("Name of the Eclipse project to create. It cannot be empty."); 414 mProjectNameField.setLayoutData(data); 415 mProjectNameField.setFont(parent.getFont()); 416 mProjectNameField.addListener(SWT.Modify, new Listener() { 417 public void handleEvent(Event event) { 418 if (!mInternalProjectNameUpdate) { 419 mProjectNameModifiedByUser = true; 420 } 421 updateLocationPathField(null); 422 } 423 }); 424 } 425 426 427 /** 428 * Creates the group for the Project options: 429 * [radio] Create new project 430 * [radio] Create project from existing sources 431 * [check] Use default location 432 * Location [text field] [browse button] 433 * 434 * @param parent the parent composite 435 */ 436 private final void createLocationGroup(Composite parent) { 437 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); 438 // Layout has 4 columns of non-equal size 439 group.setLayout(new GridLayout()); 440 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 441 group.setFont(parent.getFont()); 442 group.setText("Contents"); 443 444 mCreateNewProjectRadio = new Button(group, SWT.RADIO); 445 mCreateNewProjectRadio.setText("Create new project in workspace"); 446 mCreateNewProjectRadio.setSelection(INITIAL_CREATE_NEW_PROJECT); 447 448 Button existing_project_radio = new Button(group, SWT.RADIO); 449 existing_project_radio.setText("Create project from existing source"); 450 existing_project_radio.setSelection(INITIAL_CREATE_FROM_SOURCE); 451 452 mUseDefaultLocation = new Button(group, SWT.CHECK); 453 mUseDefaultLocation.setText("Use default location"); 454 mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION); 455 456 SelectionListener location_listener = new SelectionAdapter() { 457 @Override 458 public void widgetSelected(SelectionEvent e) { 459 super.widgetSelected(e); 460 enableLocationWidgets(); 461 extractNamesFromAndroidManifest(); 462 validatePageComplete(); 463 } 464 }; 465 466 mCreateNewProjectRadio.addSelectionListener(location_listener); 467 existing_project_radio.addSelectionListener(location_listener); 468 mUseDefaultLocation.addSelectionListener(location_listener); 469 470 Composite location_group = new Composite(group, SWT.NONE); 471 location_group.setLayout(new GridLayout(3, /* num columns */ 472 false /* columns of not equal size */)); 473 location_group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 474 location_group.setFont(parent.getFont()); 475 476 mLocationLabel = new Label(location_group, SWT.NONE); 477 mLocationLabel.setText("Location:"); 478 479 mLocationPathField = new Text(location_group, SWT.BORDER); 480 GridData data = new GridData(GridData.FILL, /* horizontal alignment */ 481 GridData.BEGINNING, /* vertical alignment */ 482 true, /* grabExcessHorizontalSpace */ 483 false, /* grabExcessVerticalSpace */ 484 1, /* horizontalSpan */ 485 1); /* verticalSpan */ 486 mLocationPathField.setLayoutData(data); 487 mLocationPathField.setFont(parent.getFont()); 488 mLocationPathField.addListener(SWT.Modify, new Listener() { 489 public void handleEvent(Event event) { 490 onLocationPathFieldModified(); 491 } 492 }); 493 494 mBrowseButton = new Button(location_group, SWT.PUSH); 495 mBrowseButton.setText("Browse..."); 496 setButtonLayoutData(mBrowseButton); 497 mBrowseButton.addSelectionListener(new SelectionAdapter() { 498 @Override 499 public void widgetSelected(SelectionEvent e) { 500 onOpenDirectoryBrowser(); 501 } 502 }); 503 504 mCreateFromSampleRadio = new Button(group, SWT.RADIO); 505 mCreateFromSampleRadio.setText("Create project from existing sample"); 506 mCreateFromSampleRadio.setSelection(INITIAL_CREATE_FROM_SAMPLE); 507 mCreateFromSampleRadio.addSelectionListener(location_listener); 508 509 Composite samples_group = new Composite(group, SWT.NONE); 510 samples_group.setLayout(new GridLayout(2, /* num columns */ 511 false /* columns of not equal size */)); 512 samples_group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 513 samples_group.setFont(parent.getFont()); 514 515 new Label(samples_group, SWT.NONE).setText("Samples:"); 516 517 mSamplesCombo = new Combo(samples_group, SWT.DROP_DOWN | SWT.READ_ONLY); 518 mSamplesCombo.setEnabled(false); 519 mSamplesCombo.select(0); 520 mSamplesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 521 mSamplesCombo.setToolTipText("Select a sample"); 522 523 mSamplesCombo.addSelectionListener(new SelectionAdapter() { 524 @Override 525 public void widgetSelected(SelectionEvent e) { 526 onSampleSelected(); 527 } 528 }); 529 530 } 531 532 /** 533 * Creates the target group. 534 * It only contains an SdkTargetSelector. 535 */ 536 private void createTargetGroup(Composite parent) { 537 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); 538 // Layout has 1 column 539 group.setLayout(new GridLayout()); 540 group.setLayoutData(new GridData(GridData.FILL_BOTH)); 541 group.setFont(parent.getFont()); 542 group.setText("Build Target"); 543 544 // The selector is created without targets. They are added below in the change listener. 545 mSdkTargetSelector = new SdkTargetSelector(group, null); 546 547 mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { 548 @Override 549 public void widgetSelected(SelectionEvent e) { 550 onSdkTargetModified(); 551 updateLocationPathField(null); 552 validatePageComplete(); 553 } 554 }); 555 556 mSdkTargetChangeListener = new ITargetChangeListener() { 557 public void onSdkLoaded() { 558 // Update the sdk target selector with the new targets 559 560 // get the targets from the sdk 561 IAndroidTarget[] targets = null; 562 if (Sdk.getCurrent() != null) { 563 targets = Sdk.getCurrent().getTargets(); 564 } 565 mSdkTargetSelector.setTargets(targets); 566 567 // If there's only one target, select it. 568 // This will invoke the selection listener on the selector defined above. 569 if (targets != null && targets.length == 1) { 570 mSdkTargetSelector.setSelection(targets[0]); 571 } 572 } 573 574 public void onProjectTargetChange(IProject changedProject) { 575 // Ignore 576 } 577 578 public void onTargetLoaded(IAndroidTarget target) { 579 // Ignore 580 } 581 }; 582 583 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); 584 } 585 586 /** 587 * Creates the group for the project properties: 588 * - Package name [text field] 589 * - Activity name [text field] 590 * - Application name [text field] 591 * 592 * @param parent the parent composite 593 */ 594 private final void createPropertiesGroup(Composite parent) { 595 // package specification group 596 Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); 597 GridLayout layout = new GridLayout(); 598 layout.numColumns = 2; 599 group.setLayout(layout); 600 group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 601 group.setFont(parent.getFont()); 602 group.setText("Properties"); 603 604 // new application label 605 Label label = new Label(group, SWT.NONE); 606 label.setText("Application name:"); 607 label.setFont(parent.getFont()); 608 label.setToolTipText("Name of the Application. This is a free string. It can be empty."); 609 610 // new application name entry field 611 mApplicationNameField = new Text(group, SWT.BORDER); 612 GridData data = new GridData(GridData.FILL_HORIZONTAL); 613 mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty."); 614 mApplicationNameField.setLayoutData(data); 615 mApplicationNameField.setFont(parent.getFont()); 616 mApplicationNameField.addListener(SWT.Modify, new Listener() { 617 public void handleEvent(Event event) { 618 if (!mInternalApplicationNameUpdate) { 619 mApplicationNameModifiedByUser = true; 620 } 621 } 622 }); 623 624 // new package label 625 label = new Label(group, SWT.NONE); 626 label.setText("Package name:"); 627 label.setFont(parent.getFont()); 628 label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components."); 629 630 // new package name entry field 631 mPackageNameField = new Text(group, SWT.BORDER); 632 data = new GridData(GridData.FILL_HORIZONTAL); 633 mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components."); 634 mPackageNameField.setLayoutData(data); 635 mPackageNameField.setFont(parent.getFont()); 636 mPackageNameField.addListener(SWT.Modify, new Listener() { 637 public void handleEvent(Event event) { 638 onPackageNameFieldModified(); 639 } 640 }); 641 642 // new activity label 643 mCreateActivityCheck = new Button(group, SWT.CHECK); 644 mCreateActivityCheck.setText("Create Activity:"); 645 mCreateActivityCheck.setToolTipText("Specifies if you want to create a default Activity."); 646 mCreateActivityCheck.setFont(parent.getFont()); 647 mCreateActivityCheck.setSelection(INITIAL_CREATE_ACTIVITY); 648 mCreateActivityCheck.addListener(SWT.Selection, new Listener() { 649 public void handleEvent(Event event) { 650 onCreateActivityCheckModified(); 651 enableLocationWidgets(); 652 } 653 }); 654 655 // new activity name entry field 656 mActivityNameField = new Text(group, SWT.BORDER); 657 data = new GridData(GridData.FILL_HORIZONTAL); 658 mActivityNameField.setToolTipText("Name of the Activity class to create. Must be a valid Java identifier."); 659 mActivityNameField.setLayoutData(data); 660 mActivityNameField.setFont(parent.getFont()); 661 mActivityNameField.addListener(SWT.Modify, new Listener() { 662 public void handleEvent(Event event) { 663 onActivityNameFieldModified(); 664 } 665 }); 666 667 // min sdk version label 668 label = new Label(group, SWT.NONE); 669 label.setText("Min SDK Version:"); 670 label.setFont(parent.getFont()); 671 label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty."); 672 673 // min sdk version entry field 674 mMinSdkVersionField = new Text(group, SWT.BORDER); 675 data = new GridData(GridData.FILL_HORIZONTAL); 676 label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty."); 677 mMinSdkVersionField.setLayoutData(data); 678 mMinSdkVersionField.setFont(parent.getFont()); 679 mMinSdkVersionField.addListener(SWT.Modify, new Listener() { 680 public void handleEvent(Event event) { 681 validatePageComplete(); 682 } 683 }); 684 } 685 686 687 //--- Internal getters & setters ------------------ 688 689 /** Returns the location path field value with spaces trimmed. */ 690 private String getLocationPathFieldValue() { 691 return mLocationPathField == null ? "" : mLocationPathField.getText().trim(); //$NON-NLS-1$ 692 } 693 694 /** Returns the current selected sample path, 695 * or an empty string if there's no valid selection. */ 696 private String getSelectedSamplePath() { 697 int selIndex = mSamplesCombo.getSelectionIndex(); 698 if (selIndex >= 0 && selIndex < mSamplesPaths.size()) { 699 return mSamplesPaths.get(selIndex); 700 } 701 return ""; 702 } 703 704 /** Returns the current project location, depending on the Use Default Location check box 705 * or the Create From Sample check box. */ 706 private String getProjectLocation() { 707 if (mInfo.isCreateFromSample()) { 708 return getSelectedSamplePath(); 709 } else if (mInfo.isNewProject() && mInfo.useDefaultLocation()) { 710 return Platform.getLocation().toString(); 711 } else { 712 return getLocationPathFieldValue(); 713 } 714 } 715 716 /** 717 * Creates a project resource handle for the current project name field 718 * value. 719 * <p> 720 * This method does not create the project resource; this is the 721 * responsibility of <code>IProject::create</code> invoked by the new 722 * project resource wizard. 723 * </p> 724 * 725 * @return the new project resource handle 726 */ 727 private IProject getProjectHandle() { 728 return ResourcesPlugin.getWorkspace().getRoot().getProject(mInfo.getProjectName()); 729 } 730 731 // --- UI Callbacks ---- 732 733 /** 734 * Display a directory browser and update the location path field with the selected path 735 */ 736 private void onOpenDirectoryBrowser() { 737 738 String existing_dir = getLocationPathFieldValue(); 739 740 // Disable the path if it doesn't exist 741 if (existing_dir.length() == 0) { 742 existing_dir = null; 743 } else { 744 File f = new File(existing_dir); 745 if (!f.exists()) { 746 existing_dir = null; 747 } 748 } 749 750 DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell()); 751 dd.setMessage("Browse for folder"); 752 dd.setFilterPath(existing_dir); 753 String abs_dir = dd.open(); 754 755 if (abs_dir != null) { 756 updateLocationPathField(abs_dir); 757 extractNamesFromAndroidManifest(); 758 validatePageComplete(); 759 } 760 } 761 762 /** 763 * A sample was selected. Update the location field, manifest and validate. 764 */ 765 private void onSampleSelected() { 766 if (mInfo.isCreateFromSample()) { 767 // Note that getProjectLocation() is automatically updated to use the currently 768 // selected sample. We just need to refresh the manifest data & validate. 769 extractNamesFromAndroidManifest(); 770 validatePageComplete(); 771 } 772 } 773 774 /** 775 * Enables or disable the location widgets depending on the user selection: 776 * the location path is enabled when using the "existing source" mode (i.e. not new project) 777 * or in new project mode with the "use default location" turned off. 778 */ 779 private void enableLocationWidgets() { 780 boolean is_new_project = mInfo.isNewProject(); 781 boolean is_create_from_sample = mInfo.isCreateFromSample(); 782 boolean use_default = mInfo.useDefaultLocation() && !is_create_from_sample; 783 boolean location_enabled = (!is_new_project || !use_default) && !is_create_from_sample; 784 boolean create_activity = mInfo.isCreateActivity(); 785 786 mUseDefaultLocation.setEnabled(is_new_project); 787 788 mLocationLabel.setEnabled(location_enabled); 789 mLocationPathField.setEnabled(location_enabled); 790 mBrowseButton.setEnabled(location_enabled); 791 792 mSamplesCombo.setEnabled(is_create_from_sample && mSamplesPaths.size() > 0); 793 794 // Most fields are only editable in new-project mode. When importing 795 // an existing project/sample we won't edit existing files anyway so the 796 // user won't be able to customize them, 797 mApplicationNameField.setEnabled(is_new_project); 798 mMinSdkVersionField.setEnabled(is_new_project); 799 mPackageNameField.setEnabled(is_new_project); 800 mCreateActivityCheck.setEnabled(is_new_project); 801 mActivityNameField.setEnabled(is_new_project & create_activity); 802 803 updateLocationPathField(null); 804 updatePackageAndActivityFields(); 805 } 806 807 /** 808 * Updates the location directory path field. 809 * <br/> 810 * When custom user selection is enabled, use the abs_dir argument if not null and also 811 * save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the 812 * user selection to be remembered when the user switches from default to custom. 813 * <br/> 814 * When custom user selection is disabled, use the workspace default location with the 815 * current project name. This does not change the internally cached abs_dir. 816 * 817 * @param abs_dir A new absolute directory path or null to use the default. 818 */ 819 private void updateLocationPathField(String abs_dir) { 820 821 // We don't touch the location path if using the "Create From Sample" mode 822 if (mInfo.isCreateFromSample()) { 823 return; 824 } 825 826 boolean is_new_project = mInfo.isNewProject(); 827 boolean use_default = mInfo.useDefaultLocation(); 828 boolean custom_location = !is_new_project || !use_default; 829 830 if (!mInternalLocationPathUpdate) { 831 mInternalLocationPathUpdate = true; 832 if (custom_location) { 833 if (abs_dir != null) { 834 // We get here if the user selected a directory with the "Browse" button. 835 // Disable auto-compute of the custom location unless the user selected 836 // the exact same path. 837 sAutoComputeCustomLocation = sAutoComputeCustomLocation && 838 abs_dir.equals(sCustomLocationOsPath); 839 sCustomLocationOsPath = TextProcessor.process(abs_dir); 840 } else if (sAutoComputeCustomLocation || 841 (!is_new_project && !new File(sCustomLocationOsPath).isDirectory())) { 842 // By default select the samples directory of the current target 843 IAndroidTarget target = mInfo.getSdkTarget(); 844 if (target != null) { 845 sCustomLocationOsPath = target.getPath(IAndroidTarget.SAMPLES); 846 } 847 848 // If we don't have a target, select the base directory of the 849 // "universal sdk". If we don't even have that, use a root drive. 850 if (sCustomLocationOsPath == null || sCustomLocationOsPath.length() == 0) { 851 if (Sdk.getCurrent() != null) { 852 sCustomLocationOsPath = Sdk.getCurrent().getSdkLocation(); 853 } else { 854 sCustomLocationOsPath = File.listRoots()[0].getAbsolutePath(); 855 } 856 } 857 } 858 if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) { 859 mLocationPathField.setText(sCustomLocationOsPath); 860 } 861 } else { 862 String value = Platform.getLocation().append(mInfo.getProjectName()).toString(); 863 value = TextProcessor.process(value); 864 if (!mLocationPathField.getText().equals(value)) { 865 mLocationPathField.setText(value); 866 } 867 } 868 validatePageComplete(); 869 mInternalLocationPathUpdate = false; 870 } 871 } 872 873 /** 874 * The location path field is either modified internally (from updateLocationPathField) 875 * or manually by the user when the custom_location mode is not set. 876 * 877 * Ignore the internal modification. When modified by the user, memorize the choice and 878 * validate the page. 879 */ 880 private void onLocationPathFieldModified() { 881 if (!mInternalLocationPathUpdate) { 882 // When the updates doesn't come from updateLocationPathField, it must be the user 883 // editing the field manually, in which case we want to save the value internally 884 // and we disable auto-compute of the custom location (to avoid overriding the user 885 // value) 886 String newPath = getLocationPathFieldValue(); 887 sAutoComputeCustomLocation = sAutoComputeCustomLocation && 888 newPath.equals(sCustomLocationOsPath); 889 sCustomLocationOsPath = newPath; 890 extractNamesFromAndroidManifest(); 891 validatePageComplete(); 892 } 893 } 894 895 /** 896 * The package name field is either modified internally (from extractNamesFromAndroidManifest) 897 * or manually by the user when the custom_location mode is not set. 898 * 899 * Ignore the internal modification. When modified by the user, memorize the choice and 900 * validate the page. 901 */ 902 private void onPackageNameFieldModified() { 903 if (mInfo.isNewProject()) { 904 mUserPackageName = mInfo.getPackageName(); 905 validatePageComplete(); 906 } 907 } 908 909 /** 910 * The create activity checkbox is either modified internally (from 911 * extractNamesFromAndroidManifest) or manually by the user. 912 * 913 * Ignore the internal modification. When modified by the user, memorize the choice and 914 * validate the page. 915 */ 916 private void onCreateActivityCheckModified() { 917 if (mInfo.isNewProject() && !mInternalCreateActivityUpdate) { 918 mUserCreateActivityCheck = mInfo.isCreateActivity(); 919 } 920 validatePageComplete(); 921 } 922 923 /** 924 * The activity name field is either modified internally (from extractNamesFromAndroidManifest) 925 * or manually by the user when the custom_location mode is not set. 926 * 927 * Ignore the internal modification. When modified by the user, memorize the choice and 928 * validate the page. 929 */ 930 private void onActivityNameFieldModified() { 931 if (mInfo.isNewProject() && !mInternalActivityNameUpdate) { 932 mUserActivityName = mInfo.getActivityName(); 933 validatePageComplete(); 934 } 935 } 936 937 /** 938 * Called when an SDK target is modified. 939 * 940 * Also changes the minSdkVersion field to reflect the sdk api level that has 941 * just been selected. 942 */ 943 private void onSdkTargetModified() { 944 IAndroidTarget target = mInfo.getSdkTarget(); 945 946 loadSamplesForTarget(target); 947 enableLocationWidgets(); 948 onSampleSelected(); 949 } 950 951 /** 952 * Called when the radio buttons are changed between the "create new project" and the 953 * "use existing source" mode. This reverts the fields to whatever the user manually 954 * entered before. 955 */ 956 private void updatePackageAndActivityFields() { 957 if (mInfo.isNewProject()) { 958 if (mUserPackageName.length() > 0 && 959 !mPackageNameField.getText().equals(mUserPackageName)) { 960 mPackageNameField.setText(mUserPackageName); 961 } 962 963 if (mUserActivityName.length() > 0 && 964 !mActivityNameField.getText().equals(mUserActivityName)) { 965 mInternalActivityNameUpdate = true; 966 mActivityNameField.setText(mUserActivityName); 967 mInternalActivityNameUpdate = false; 968 } 969 970 if (mUserCreateActivityCheck != mCreateActivityCheck.getSelection()) { 971 mInternalCreateActivityUpdate = true; 972 mCreateActivityCheck.setSelection(mUserCreateActivityCheck); 973 mInternalCreateActivityUpdate = false; 974 } 975 } 976 } 977 978 /** 979 * Extract names from an android manifest. 980 * This is done only if the user selected the "use existing source" and a manifest xml file 981 * can actually be found in the custom user directory. 982 */ 983 private void extractNamesFromAndroidManifest() { 984 if (mInfo.isNewProject()) { 985 return; 986 } 987 988 String projectLocation = getProjectLocation(); 989 File f = new File(projectLocation); 990 if (!f.isDirectory()) { 991 return; 992 } 993 994 Path path = new Path(f.getPath()); 995 String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); 996 997 ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); 998 if (manifestData == null) { 999 return; 1000 } 1001 1002 String packageName = null; 1003 Activity activity = null; 1004 String activityName = null; 1005 String minSdkVersion = null; 1006 try { 1007 packageName = manifestData.getPackage(); 1008 minSdkVersion = manifestData.getMinSdkVersionString(); 1009 1010 // try to get the first launcher activity. If none, just take the first activity. 1011 activity = manifestData.getLauncherActivity(); 1012 if (activity == null) { 1013 Activity[] activities = manifestData.getActivities(); 1014 if (activities != null && activities.length > 0) { 1015 activity = activities[0]; 1016 } 1017 } 1018 } catch (Exception e) { 1019 // ignore exceptions 1020 } 1021 1022 if (packageName != null && packageName.length() > 0) { 1023 mPackageNameField.setText(packageName); 1024 } 1025 1026 if (activity != null) { 1027 activityName = AndroidManifest.extractActivityName(activity.getName(), packageName); 1028 } 1029 1030 if (activityName != null && activityName.length() > 0) { 1031 mInternalActivityNameUpdate = true; 1032 mInternalCreateActivityUpdate = true; 1033 mActivityNameField.setText(activityName); 1034 // we are "importing" an existing activity, not creating a new one 1035 mCreateActivityCheck.setSelection(false); 1036 mInternalCreateActivityUpdate = false; 1037 mInternalActivityNameUpdate = false; 1038 1039 // If project name and application names are empty, use the activity 1040 // name as a default. If the activity name has dots, it's a part of a 1041 // package specification and only the last identifier must be used. 1042 if (activityName.indexOf('.') != -1) { 1043 String[] ids = activityName.split(AndroidConstants.RE_DOT); 1044 activityName = ids[ids.length - 1]; 1045 } 1046 if (mProjectNameField.getText().length() == 0 || !mProjectNameModifiedByUser) { 1047 mInternalProjectNameUpdate = true; 1048 mProjectNameModifiedByUser = false; 1049 mProjectNameField.setText(activityName); 1050 mInternalProjectNameUpdate = false; 1051 } 1052 if (mApplicationNameField.getText().length() == 0 || !mApplicationNameModifiedByUser) { 1053 mInternalApplicationNameUpdate = true; 1054 mApplicationNameModifiedByUser = false; 1055 mApplicationNameField.setText(activityName); 1056 mInternalApplicationNameUpdate = false; 1057 } 1058 } else { 1059 mInternalActivityNameUpdate = true; 1060 mInternalCreateActivityUpdate = true; 1061 mActivityNameField.setText(""); //$NON-NLS-1$ 1062 mCreateActivityCheck.setSelection(false); 1063 mInternalCreateActivityUpdate = false; 1064 mInternalActivityNameUpdate = false; 1065 1066 // There is no activity name to use to fill in the project and application 1067 // name. However if there's a package name, we can use this as a base. 1068 if (packageName != null && packageName.length() > 0) { 1069 // Package name is a java identifier, so it's most suitable for 1070 // an application name. 1071 1072 if (mApplicationNameField.getText().length() == 0 || 1073 !mApplicationNameModifiedByUser) { 1074 mInternalApplicationNameUpdate = true; 1075 mApplicationNameField.setText(packageName); 1076 mInternalApplicationNameUpdate = false; 1077 } 1078 1079 // For the project name, remove any dots 1080 packageName = packageName.replace('.', '_'); 1081 if (mProjectNameField.getText().length() == 0 || !mProjectNameModifiedByUser) { 1082 mInternalProjectNameUpdate = true; 1083 mProjectNameField.setText(packageName); 1084 mInternalProjectNameUpdate = false; 1085 } 1086 1087 } 1088 } 1089 1090 // Select the target matching the manifest's sdk or build properties, if any 1091 IAndroidTarget foundTarget = null; 1092 // This is the target currently in the UI 1093 IAndroidTarget currentTarget = mInfo.getSdkTarget(); 1094 1095 // If there's a current target defined, we do not allow to change it when 1096 // operating in the create-from-sample mode -- since the available sample list 1097 // is tied to the current target, so changing it would invalidate the project we're 1098 // trying to load in the first place. 1099 if (currentTarget == null || !mInfo.isCreateFromSample()) { 1100 ProjectPropertiesWorkingCopy p = ProjectProperties.create(projectLocation, null); 1101 if (p != null) { 1102 // Check the {build|default}.properties files if present 1103 p.merge(PropertyType.BUILD).merge(PropertyType.DEFAULT); 1104 String v = p.getProperty(ProjectProperties.PROPERTY_TARGET); 1105 IAndroidTarget desiredTarget = Sdk.getCurrent().getTargetFromHashString(v); 1106 // We can change the current target if: 1107 // - we found a new desired target 1108 // - there is no current target 1109 // - or the current target can't run the desired target 1110 if (desiredTarget != null && 1111 (currentTarget == null || !desiredTarget.canRunOn(currentTarget))) { 1112 foundTarget = desiredTarget; 1113 } 1114 } 1115 1116 if (foundTarget == null && minSdkVersion != null) { 1117 // Otherwise try to match the requested min-sdk-version if we find an 1118 // exact match, regardless of the currently selected target. 1119 for (IAndroidTarget existingTarget : mSdkTargetSelector.getTargets()) { 1120 if (existingTarget != null && 1121 existingTarget.getVersion().equals(minSdkVersion)) { 1122 foundTarget = existingTarget; 1123 break; 1124 } 1125 } 1126 } 1127 1128 if (foundTarget == null) { 1129 // Or last attempt, try to match a sample project location and use it 1130 // if we find an exact match, regardless of the currently selected target. 1131 for (IAndroidTarget existingTarget : mSdkTargetSelector.getTargets()) { 1132 if (existingTarget != null && 1133 projectLocation.startsWith(existingTarget.getLocation())) { 1134 foundTarget = existingTarget; 1135 break; 1136 } 1137 } 1138 } 1139 } 1140 1141 if (foundTarget != null) { 1142 mSdkTargetSelector.setSelection(foundTarget); 1143 } 1144 1145 // It's OK for an import to not a minSdkVersion and we should respect it. 1146 mMinSdkVersionField.setText(minSdkVersion == null ? "" : minSdkVersion); //$NON-NLS-1$ 1147 } 1148 1149 /** 1150 * Updates the list of all samples for the given target SDK. 1151 * The list is stored in mSamplesPaths as absolute directory paths. 1152 * The combo is recreated to match this. 1153 */ 1154 private void loadSamplesForTarget(IAndroidTarget target) { 1155 1156 // Keep the name of the old selection (if there were any samples) 1157 String oldChoice = null; 1158 if (mSamplesPaths.size() > 0) { 1159 int selIndex = mSamplesCombo.getSelectionIndex(); 1160 if (selIndex > -1) { 1161 oldChoice = mSamplesCombo.getItem(selIndex); 1162 } 1163 } 1164 1165 // Clear all current content 1166 mSamplesCombo.removeAll(); 1167 mSamplesPaths.clear(); 1168 1169 if (target != null) { 1170 // Get the sample root path and recompute the list of samples 1171 String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); 1172 1173 File samplesDir = new File(samplesRootPath); 1174 findSamplesManifests(samplesDir, mSamplesPaths); 1175 1176 if (mSamplesPaths.size() == 0) { 1177 // Odd, this target has no samples. Could happen with an addon. 1178 mSamplesCombo.add("This target has no samples. Please select another target."); 1179 mSamplesCombo.select(0); 1180 return; 1181 } 1182 1183 // Recompute the description of each sample (the relative path 1184 // to the sample root). Also try to find the old selection. 1185 int selIndex = 0; 1186 int i = 0; 1187 int n = samplesRootPath.length(); 1188 for (String path : mSamplesPaths) { 1189 if (path.length() > n) { 1190 path = path.substring(n); 1191 if (path.charAt(0) == File.separatorChar) { 1192 path = path.substring(1); 1193 } 1194 if (path.endsWith(File.separator)) { 1195 path = path.substring(0, path.length() - 1); 1196 } 1197 path = path.replaceAll(Pattern.quote(File.separator), " > "); 1198 } 1199 1200 if (oldChoice != null && oldChoice.equals(path)) { 1201 selIndex = i; 1202 } 1203 1204 mSamplesCombo.add(path); 1205 i++; 1206 } 1207 1208 mSamplesCombo.select(selIndex); 1209 1210 } else { 1211 mSamplesCombo.add("Please select a target."); 1212 mSamplesCombo.select(0); 1213 } 1214 } 1215 1216 /** 1217 * Recursively find potential sample directories under the given directory. 1218 * Actually lists any directory that contains an android manifest. 1219 * Paths found are added the samplesPaths list. 1220 */ 1221 private void findSamplesManifests(File samplesDir, ArrayList<String> samplesPaths) { 1222 if (!samplesDir.isDirectory()) { 1223 return; 1224 } 1225 1226 for (File f : samplesDir.listFiles()) { 1227 if (f.isDirectory()) { 1228 // Assume this is a sample if it contains an android manifest. 1229 File manifestFile = new File(f, SdkConstants.FN_ANDROID_MANIFEST_XML); 1230 if (manifestFile.isFile()) { 1231 samplesPaths.add(f.getPath()); 1232 } 1233 1234 // Recurse in the project, to find embedded tests sub-projects 1235 // We can however skip this recursion for known android sub-dirs that 1236 // can't have projects, namely for sources, assets and resources. 1237 String leaf = f.getName(); 1238 if (!SdkConstants.FD_SOURCES.equals(leaf) && 1239 !SdkConstants.FD_ASSETS.equals(leaf) && 1240 !SdkConstants.FD_RES.equals(leaf)) { 1241 findSamplesManifests(f, samplesPaths); 1242 } 1243 } 1244 } 1245 } 1246 1247 /** 1248 * Returns whether this page's controls currently all contain valid values. 1249 * 1250 * @return <code>true</code> if all controls are valid, and 1251 * <code>false</code> if at least one is invalid 1252 */ 1253 private boolean validatePage() { 1254 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 1255 1256 int status = validateProjectField(workspace); 1257 if ((status & MSG_ERROR) == 0) { 1258 status |= validateSdkTarget(); 1259 } 1260 if ((status & MSG_ERROR) == 0) { 1261 status |= validateLocationPath(workspace); 1262 } 1263 if ((status & MSG_ERROR) == 0) { 1264 status |= validatePackageField(); 1265 } 1266 if ((status & MSG_ERROR) == 0) { 1267 status |= validateActivityField(); 1268 } 1269 if ((status & MSG_ERROR) == 0) { 1270 status |= validateMinSdkVersionField(); 1271 } 1272 if ((status & MSG_ERROR) == 0) { 1273 status |= validateSourceFolder(); 1274 } 1275 if (status == MSG_NONE) { 1276 setStatus(null, MSG_NONE); 1277 } 1278 1279 // Return false if there's an error so that the finish button be disabled. 1280 return (status & MSG_ERROR) == 0; 1281 } 1282 1283 /** 1284 * Validates the page and updates the Next/Finish buttons 1285 */ 1286 private void validatePageComplete() { 1287 setPageComplete(validatePage()); 1288 } 1289 1290 /** 1291 * Validates the project name field. 1292 * 1293 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1294 */ 1295 private int validateProjectField(IWorkspace workspace) { 1296 // Validate project field 1297 String projectName = mInfo.getProjectName(); 1298 if (projectName.length() == 0) { 1299 return setStatus("Project name must be specified", MSG_ERROR); 1300 } 1301 1302 // Limit the project name to shell-agnostic characters since it will be used to 1303 // generate the final package 1304 if (!sProjectNamePattern.matcher(projectName).matches()) { 1305 return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.", 1306 MSG_ERROR); 1307 } 1308 1309 IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT); 1310 if (!nameStatus.isOK()) { 1311 return setStatus(nameStatus.getMessage(), MSG_ERROR); 1312 } 1313 1314 if (getProjectHandle().exists()) { 1315 return setStatus("A project with that name already exists in the workspace", 1316 MSG_ERROR); 1317 } 1318 1319 if (mTestInfo != null && 1320 mTestInfo.getCreateTestProject() && 1321 projectName.equals(mTestInfo.getProjectName())) { 1322 return setStatus("The main project name and the test project name must be different.", 1323 MSG_WARNING); 1324 } 1325 1326 return MSG_NONE; 1327 } 1328 1329 /** 1330 * Validates the location path field. 1331 * 1332 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1333 */ 1334 private int validateLocationPath(IWorkspace workspace) { 1335 Path path = new Path(getProjectLocation()); 1336 if (mInfo.isNewProject()) { 1337 if (!mInfo.useDefaultLocation()) { 1338 // If not using the default value validate the location. 1339 URI uri = URIUtil.toURI(path.toOSString()); 1340 IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(), 1341 uri); 1342 if (!locationStatus.isOK()) { 1343 return setStatus(locationStatus.getMessage(), MSG_ERROR); 1344 } else { 1345 // The location is valid as far as Eclipse is concerned (i.e. mostly not 1346 // an existing workspace project.) Check it either doesn't exist or is 1347 // a directory that is empty. 1348 File f = path.toFile(); 1349 if (f.exists() && !f.isDirectory()) { 1350 return setStatus("A directory name must be specified.", MSG_ERROR); 1351 } else if (f.isDirectory()) { 1352 // However if the directory exists, we should put a warning if it is not 1353 // empty. We don't put an error (we'll ask the user again for confirmation 1354 // before using the directory.) 1355 String[] l = f.list(); 1356 if (l.length != 0) { 1357 return setStatus("The selected output directory is not empty.", 1358 MSG_WARNING); 1359 } 1360 } 1361 } 1362 } else { 1363 // Otherwise validate the path string is not empty 1364 if (getProjectLocation().length() == 0) { 1365 return setStatus("A directory name must be specified.", MSG_ERROR); 1366 } 1367 1368 File dest = path.append(mInfo.getProjectName()).toFile(); 1369 if (dest.exists()) { 1370 return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.", 1371 mInfo.getProjectName()), MSG_ERROR); 1372 } 1373 } 1374 } else { 1375 // Must be an existing directory 1376 File f = path.toFile(); 1377 if (!f.isDirectory()) { 1378 return setStatus("An existing directory name must be specified.", MSG_ERROR); 1379 } 1380 1381 // Check there's an android manifest in the directory 1382 String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); 1383 File manifestFile = new File(osPath); 1384 if (!manifestFile.isFile()) { 1385 return setStatus( 1386 String.format("File %1$s not found in %2$s.", 1387 SdkConstants.FN_ANDROID_MANIFEST_XML, f.getName()), 1388 MSG_ERROR); 1389 } 1390 1391 // Parse it and check the important fields. 1392 ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); 1393 if (manifestData == null) { 1394 return setStatus( 1395 String.format("File %1$s could not be parsed.", osPath), 1396 MSG_ERROR); 1397 } 1398 1399 String packageName = manifestData.getPackage(); 1400 if (packageName == null || packageName.length() == 0) { 1401 return setStatus( 1402 String.format("No package name defined in %1$s.", osPath), 1403 MSG_ERROR); 1404 } 1405 1406 Activity[] activities = manifestData.getActivities(); 1407 if (activities == null || activities.length == 0) { 1408 // This is acceptable now as long as no activity needs to be created 1409 if (mInfo.isCreateActivity()) { 1410 return setStatus( 1411 String.format("No activity name defined in %1$s.", osPath), 1412 MSG_ERROR); 1413 } 1414 } 1415 1416 // If there's already a .project, tell the user to use import instead. 1417 if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ 1418 return setStatus("An Eclipse project already exists in this directory. Consider using File > Import > Existing Project instead.", 1419 MSG_WARNING); 1420 } 1421 } 1422 1423 return MSG_NONE; 1424 } 1425 1426 /** 1427 * Validates the sdk target choice. 1428 * 1429 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1430 */ 1431 private int validateSdkTarget() { 1432 if (mInfo.getSdkTarget() == null) { 1433 return setStatus("An SDK Target must be specified.", MSG_ERROR); 1434 } 1435 return MSG_NONE; 1436 } 1437 1438 /** 1439 * Validates the sdk target choice. 1440 * 1441 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1442 */ 1443 private int validateMinSdkVersionField() { 1444 1445 // If the current target is a preview, explicitly indicate minSdkVersion 1446 // must be set to this target name. 1447 // Since the field is only editable in new-project mode, we can't produce an 1448 // error when importing an existing project. 1449 if (mInfo.isNewProject() && 1450 mInfo.getSdkTarget() != null && 1451 mInfo.getSdkTarget().getVersion().isPreview() && 1452 mInfo.getSdkTarget().getVersion().equals(mInfo.getMinSdkVersion()) == false) { 1453 return setStatus( 1454 String.format("The SDK target is a preview. Min SDK Version must be set to '%s'.", 1455 mInfo.getSdkTarget().getVersion().getCodename()), 1456 MSG_ERROR); 1457 } 1458 1459 // If the min sdk version is empty, it is always accepted. 1460 if (mInfo.getMinSdkVersion().length() == 0) { 1461 return MSG_NONE; 1462 } 1463 1464 if (mInfo.getSdkTarget() != null && 1465 mInfo.getSdkTarget().getVersion().equals(mInfo.getMinSdkVersion()) == false) { 1466 return setStatus("The API level for the selected SDK target does not match the Min SDK Version.", 1467 mInfo.getSdkTarget().getVersion().isPreview() ? MSG_ERROR : MSG_WARNING); 1468 } 1469 1470 return MSG_NONE; 1471 } 1472 1473 /** 1474 * Validates the activity name field. 1475 * 1476 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1477 */ 1478 private int validateActivityField() { 1479 // Disregard if not creating an activity 1480 if (!mInfo.isCreateActivity()) { 1481 return MSG_NONE; 1482 } 1483 1484 // Validate activity field 1485 String activityFieldContents = mInfo.getActivityName(); 1486 if (activityFieldContents.length() == 0) { 1487 return setStatus("Activity name must be specified.", MSG_ERROR); 1488 } 1489 1490 // The activity field can actually contain part of a sub-package name 1491 // or it can start with a dot "." to indicates it comes from the parent package name. 1492 String packageName = ""; //$NON-NLS-1$ 1493 int pos = activityFieldContents.lastIndexOf('.'); 1494 if (pos >= 0) { 1495 packageName = activityFieldContents.substring(0, pos); 1496 if (packageName.startsWith(".")) { //$NON-NLS-1$ 1497 packageName = packageName.substring(1); 1498 } 1499 1500 activityFieldContents = activityFieldContents.substring(pos + 1); 1501 } 1502 1503 // the activity field can contain a simple java identifier, or a 1504 // package name or one that starts with a dot. So if it starts with a dot, 1505 // ignore this dot -- the rest must look like a package name. 1506 if (activityFieldContents.charAt(0) == '.') { 1507 activityFieldContents = activityFieldContents.substring(1); 1508 } 1509 1510 // Check it's a valid activity string 1511 int result = MSG_NONE; 1512 IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents, 1513 "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ 1514 if (!status.isOK()) { 1515 result = setStatus(status.getMessage(), 1516 status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); 1517 } 1518 1519 // Check it's a valid package string 1520 if (result != MSG_ERROR && packageName.length() > 0) { 1521 status = JavaConventions.validatePackageName(packageName, 1522 "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ 1523 if (!status.isOK()) { 1524 result = setStatus(status.getMessage() + " (in the activity name)", 1525 status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); 1526 } 1527 } 1528 1529 1530 return result; 1531 } 1532 1533 /** 1534 * Validates the package name field. 1535 * 1536 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1537 */ 1538 private int validatePackageField() { 1539 // Validate package field 1540 String packageFieldContents = mInfo.getPackageName(); 1541 if (packageFieldContents.length() == 0) { 1542 return setStatus("Package name must be specified.", MSG_ERROR); 1543 } 1544 1545 // Check it's a valid package string 1546 int result = MSG_NONE; 1547 IStatus status = JavaConventions.validatePackageName(packageFieldContents, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ 1548 if (!status.isOK()) { 1549 result = setStatus(status.getMessage(), 1550 status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); 1551 } 1552 1553 // The Android Activity Manager does not accept packages names with only one 1554 // identifier. Check the package name has at least one dot in them (the previous rule 1555 // validated that if such a dot exist, it's not the first nor last characters of the 1556 // string.) 1557 if (result != MSG_ERROR && packageFieldContents.indexOf('.') == -1) { 1558 return setStatus("Package name must have at least two identifiers.", MSG_ERROR); 1559 } 1560 1561 return result; 1562 } 1563 1564 /** 1565 * Validates that an existing project actually has a source folder. 1566 * 1567 * For project in "use existing source" mode, this tries to find the source folder. 1568 * A source folder should be just under the project directory and it should have all 1569 * the directories composing the package+activity name. 1570 * 1571 * As a side effect, it memorizes the source folder in mSourceFolder. 1572 * 1573 * TODO: support multiple source folders for multiple activities. 1574 * 1575 * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. 1576 */ 1577 private int validateSourceFolder() { 1578 // This check does nothing when creating a new project. 1579 // This check is also useless when no activity is present or created. 1580 if (mInfo.isNewProject() || !mInfo.isCreateActivity()) { 1581 return MSG_NONE; 1582 } 1583 1584 String osTarget = mInfo.getActivityName(); 1585 1586 if (osTarget.indexOf('.') == -1) { 1587 osTarget = mInfo.getPackageName() + File.separator + osTarget; 1588 } else if (osTarget.indexOf('.') == 0) { 1589 osTarget = mInfo.getPackageName() + osTarget; 1590 } 1591 osTarget = osTarget.replace('.', File.separatorChar) + AndroidConstants.DOT_JAVA; 1592 1593 String projectPath = getProjectLocation(); 1594 File projectDir = new File(projectPath); 1595 File[] all_dirs = projectDir.listFiles(new FileFilter() { 1596 public boolean accept(File pathname) { 1597 return pathname.isDirectory(); 1598 } 1599 }); 1600 for (File f : all_dirs) { 1601 Path path = new Path(f.getAbsolutePath()); 1602 File java_activity = path.append(osTarget).toFile(); 1603 if (java_activity.isFile()) { 1604 mSourceFolder = f.getName(); 1605 return MSG_NONE; 1606 } 1607 } 1608 1609 if (all_dirs.length > 0) { 1610 return setStatus( 1611 String.format("%1$s can not be found under %2$s.", osTarget, projectPath), 1612 MSG_ERROR); 1613 } else { 1614 return setStatus( 1615 String.format("No source folders can be found in %1$s.", projectPath), 1616 MSG_ERROR); 1617 } 1618 } 1619 1620 /** 1621 * Sets the error message for the wizard with the given message icon. 1622 * 1623 * @param message The wizard message type, one of MSG_ERROR or MSG_WARNING. 1624 * @return As a convenience, always returns messageType so that the caller can return 1625 * immediately. 1626 */ 1627 private int setStatus(String message, int messageType) { 1628 if (message == null) { 1629 setErrorMessage(null); 1630 setMessage(null); 1631 } else if (!message.equals(getMessage())) { 1632 setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR); 1633 } 1634 return messageType; 1635 } 1636 1637 } 1638