Home | History | Annotate | Download | only in newproject
      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