blob: 20aa68bdd0106559b67c4a70c43a2cf0b37f6129 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* References:
* org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard
* org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage
*/
package com.android.ide.eclipse.adt.wizards.newproject;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.AndroidManifestParser.Activity;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.project.ProjectProperties;
import com.android.sdklib.project.ProjectProperties.PropertyType;
import com.android.sdkuilib.SdkTargetSelector;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import java.io.File;
import java.io.FileFilter;
import java.net.URI;
import java.util.regex.Pattern;
/**
* NewAndroidProjectCreationPage is a project creation page that provides the
* following fields:
* <ul>
* <li> Project name
* <li> SDK Target
* <li> Application name
* <li> Package name
* <li> Activity name
* </ul>
* Note: this class is public so that it can be accessed from unit tests.
* It is however an internal class. Its API may change without notice.
* It should semantically be considered as a private final class.
* Do not derive from this class.
*/
public class NewProjectCreationPage extends WizardPage {
// constants
/** Initial value for all name fields (project, activity, application, package). Used
* whenever a value is requested before controls are created. */
private static final String INITIAL_NAME = ""; //$NON-NLS-1$
/** Initial value for the Create New Project radio; False means Create From Existing would be
* the default.*/
private static final boolean INITIAL_CREATE_NEW_PROJECT = true;
/** Initial value for the Use Default Location check box. */
private static final boolean INITIAL_USE_DEFAULT_LOCATION = true;
/** Initial value for the Create Activity check box. */
private static final boolean INITIAL_CREATE_ACTIVITY = true;
/** Pattern for characters accepted in a project name. Since this will be used as a
* directory name, we're being a bit conservative on purpose. It cannot start with a space. */
private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$
/** Last user-browsed location, static so that it be remembered for the whole session */
private static String sCustomLocationOsPath = ""; //$NON-NLS-1$
private static boolean sAutoComputeCustomLocation = true;
private final int MSG_NONE = 0;
private final int MSG_WARNING = 1;
private final int MSG_ERROR = 2;
private String mUserPackageName = ""; //$NON-NLS-1$
private String mUserActivityName = ""; //$NON-NLS-1$
private boolean mUserCreateActivityCheck = INITIAL_CREATE_ACTIVITY;
private String mSourceFolder = ""; //$NON-NLS-1$
// widgets
private Text mProjectNameField;
private Text mPackageNameField;
private Text mActivityNameField;
private Text mApplicationNameField;
private Button mCreateNewProjectRadio;
private Button mUseDefaultLocation;
private Label mLocationLabel;
private Text mLocationPathField;
private Button mBrowseButton;
private Button mCreateActivityCheck;
private Text mMinSdkVersionField;
private SdkTargetSelector mSdkTargetSelector;
private ITargetChangeListener mSdkTargetChangeListener;
private boolean mInternalLocationPathUpdate;
protected boolean mInternalProjectNameUpdate;
protected boolean mInternalApplicationNameUpdate;
private boolean mInternalCreateActivityUpdate;
private boolean mInternalActivityNameUpdate;
protected boolean mProjectNameModifiedByUser;
protected boolean mApplicationNameModifiedByUser;
private boolean mInternalMinSdkVersionUpdate;
private boolean mMinSdkVersionModifiedByUser;
/**
* Creates a new project creation wizard page.
*
* @param pageName the name of this page
*/
public NewProjectCreationPage(String pageName) {
super(pageName);
setPageComplete(false);
}
// --- Getters used by NewProjectWizard ---
/**
* Returns the current project location path as entered by the user, or its
* anticipated initial value. Note that if the default has been returned the
* path in a project description used to create a project should not be set.
*
* @return the project location path or its anticipated initial value.
*/
public IPath getLocationPath() {
return new Path(getProjectLocation());
}
/** Returns the value of the project name field with leading and trailing spaces removed. */
public String getProjectName() {
return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim();
}
/** Returns the value of the package name field with spaces trimmed. */
public String getPackageName() {
return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim();
}
/** Returns the value of the activity name field with spaces trimmed. */
public String getActivityName() {
return mActivityNameField == null ? INITIAL_NAME : mActivityNameField.getText().trim();
}
/** Returns the value of the min sdk version field with spaces trimmed. */
public String getMinSdkVersion() {
return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim();
}
/** Returns the value of the application name field with spaces trimmed. */
public String getApplicationName() {
// Return the name of the activity as default application name.
return mApplicationNameField == null ? getActivityName()
: mApplicationNameField.getText().trim();
}
/** Returns the value of the "Create New Project" radio. */
public boolean isNewProject() {
return mCreateNewProjectRadio == null ? INITIAL_CREATE_NEW_PROJECT
: mCreateNewProjectRadio.getSelection();
}
/** Returns the value of the "Create Activity" checkbox. */
public boolean isCreateActivity() {
return mCreateActivityCheck == null ? INITIAL_CREATE_ACTIVITY
: mCreateActivityCheck.getSelection();
}
/** Returns the value of the Use Default Location field. */
public boolean useDefaultLocation() {
return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION
: mUseDefaultLocation.getSelection();
}
/** Returns the internal source folder (for the "existing project" mode) or the default
* "src" constant. */
public String getSourceFolder() {
if (isNewProject() || mSourceFolder == null || mSourceFolder.length() == 0) {
return SdkConstants.FD_SOURCES;
} else {
return mSourceFolder;
}
}
/** Returns the current sdk target or null if none has been selected yet. */
public IAndroidTarget getSdkTarget() {
return mSdkTargetSelector == null ? null : mSdkTargetSelector.getSelected();
}
/**
* Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when
* the dialog is made visible.
*/
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
mProjectNameField.setFocus();
}
}
// --- UI creation ---
/**
* Creates the top level control for this dialog page under the given parent
* composite.
*
* @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
*/
public void createControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setFont(parent.getFont());
initializeDialogUnits(parent);
composite.setLayout(new GridLayout());
composite.setLayoutData(new GridData(GridData.FILL_BOTH));
createProjectNameGroup(composite);
createLocationGroup(composite);
createTargetGroup(composite);
createPropertiesGroup(composite);
// Update state the first time
enableLocationWidgets();
// Show description the first time
setErrorMessage(null);
setMessage(null);
setControl(composite);
// Validate. This will complain about the first empty field.
setPageComplete(validatePage());
}
@Override
public void dispose() {
if (mSdkTargetChangeListener != null) {
AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
mSdkTargetChangeListener = null;
}
super.dispose();
}
/**
* Creates the group for the project name:
* [label: "Project Name"] [text field]
*
* @param parent the parent composite
*/
private final void createProjectNameGroup(Composite parent) {
Composite group = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
// new project label
Label label = new Label(group, SWT.NONE);
label.setText("Project name:");
label.setFont(parent.getFont());
label.setToolTipText("Name of the Eclipse project to create. It cannot be empty.");
// new project name entry field
mProjectNameField = new Text(group, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
mProjectNameField.setToolTipText("Name of the Eclipse project to create. It cannot be empty.");
mProjectNameField.setLayoutData(data);
mProjectNameField.setFont(parent.getFont());
mProjectNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
if (!mInternalProjectNameUpdate) {
mProjectNameModifiedByUser = true;
}
updateLocationPathField(null);
}
});
}
/**
* Creates the group for the Project options:
* [radio] Create new project
* [radio] Create project from existing sources
* [check] Use default location
* Location [text field] [browse button]
*
* @param parent the parent composite
*/
private final void createLocationGroup(Composite parent) {
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
// Layout has 4 columns of non-equal size
group.setLayout(new GridLayout());
group.setLayoutData(new GridData(GridData.FILL_BOTH));
group.setFont(parent.getFont());
group.setText("Contents");
mCreateNewProjectRadio = new Button(group, SWT.RADIO);
mCreateNewProjectRadio.setText("Create new project in workspace");
mCreateNewProjectRadio.setSelection(INITIAL_CREATE_NEW_PROJECT);
Button existing_project_radio = new Button(group, SWT.RADIO);
existing_project_radio.setText("Create project from existing source");
existing_project_radio.setSelection(!INITIAL_CREATE_NEW_PROJECT);
mUseDefaultLocation = new Button(group, SWT.CHECK);
mUseDefaultLocation.setText("Use default location");
mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION);
SelectionListener location_listener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
super.widgetSelected(e);
enableLocationWidgets();
extractNamesFromAndroidManifest();
setPageComplete(validatePage());
}
};
mCreateNewProjectRadio.addSelectionListener(location_listener);
existing_project_radio.addSelectionListener(location_listener);
mUseDefaultLocation.addSelectionListener(location_listener);
Composite location_group = new Composite(group, SWT.NONE);
location_group.setLayout(new GridLayout(4, /* num columns */
false /* columns of not equal size */));
location_group.setLayoutData(new GridData(GridData.FILL_BOTH));
location_group.setFont(parent.getFont());
mLocationLabel = new Label(location_group, SWT.NONE);
mLocationLabel.setText("Location:");
mLocationPathField = new Text(location_group, SWT.BORDER);
GridData data = new GridData(GridData.FILL, /* horizontal alignment */
GridData.BEGINNING, /* vertical alignment */
true, /* grabExcessHorizontalSpace */
false, /* grabExcessVerticalSpace */
2, /* horizontalSpan */
1); /* verticalSpan */
mLocationPathField.setLayoutData(data);
mLocationPathField.setFont(parent.getFont());
mLocationPathField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onLocationPathFieldModified();
}
});
mBrowseButton = new Button(location_group, SWT.PUSH);
mBrowseButton.setText("Browse...");
setButtonLayoutData(mBrowseButton);
mBrowseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
openDirectoryBrowser();
}
});
}
/**
* Creates the target group.
* It only contains an SdkTargetSelector.
*/
private void createTargetGroup(Composite parent) {
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
// Layout has 1 column
group.setLayout(new GridLayout());
group.setLayoutData(new GridData(GridData.FILL_BOTH));
group.setFont(parent.getFont());
group.setText("Build Target");
// The selector is created without targets. They are added below in the change listener.
mSdkTargetSelector = new SdkTargetSelector(group, null);
mSdkTargetChangeListener = new ITargetChangeListener() {
public void onProjectTargetChange(IProject changedProject) {
// Ignore
}
public void onTargetsLoaded() {
// Update the sdk target selector with the new targets
// get the targets from the sdk
IAndroidTarget[] targets = null;
if (Sdk.getCurrent() != null) {
targets = Sdk.getCurrent().getTargets();
}
mSdkTargetSelector.setTargets(targets);
// If there's only one target, select it
if (targets != null && targets.length == 1) {
mSdkTargetSelector.setSelection(targets[0]);
}
}
};
AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
// Invoke it once to initialize the targets
mSdkTargetChangeListener.onTargetsLoaded();
mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onSdkTargetModified();
updateLocationPathField(null);
setPageComplete(validatePage());
}
});
}
/**
* Display a directory browser and update the location path field with the selected path
*/
private void openDirectoryBrowser() {
String existing_dir = getLocationPathFieldValue();
// Disable the path if it doesn't exist
if (existing_dir.length() == 0) {
existing_dir = null;
} else {
File f = new File(existing_dir);
if (!f.exists()) {
existing_dir = null;
}
}
DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell());
dd.setMessage("Browse for folder");
dd.setFilterPath(existing_dir);
String abs_dir = dd.open();
if (abs_dir != null) {
updateLocationPathField(abs_dir);
extractNamesFromAndroidManifest();
setPageComplete(validatePage());
}
}
/**
* Creates the group for the project properties:
* - Package name [text field]
* - Activity name [text field]
* - Application name [text field]
*
* @param parent the parent composite
*/
private final void createPropertiesGroup(Composite parent) {
// package specification group
Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
group.setLayout(layout);
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
group.setFont(parent.getFont());
group.setText("Properties");
// new application label
Label label = new Label(group, SWT.NONE);
label.setText("Application name:");
label.setFont(parent.getFont());
label.setToolTipText("Name of the Application. This is a free string. It can be empty.");
// new application name entry field
mApplicationNameField = new Text(group, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty.");
mApplicationNameField.setLayoutData(data);
mApplicationNameField.setFont(parent.getFont());
mApplicationNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
if (!mInternalApplicationNameUpdate) {
mApplicationNameModifiedByUser = true;
}
}
});
// new package label
label = new Label(group, SWT.NONE);
label.setText("Package name:");
label.setFont(parent.getFont());
label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
// new package name entry field
mPackageNameField = new Text(group, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
mPackageNameField.setLayoutData(data);
mPackageNameField.setFont(parent.getFont());
mPackageNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onPackageNameFieldModified();
}
});
// new activity label
mCreateActivityCheck = new Button(group, SWT.CHECK);
mCreateActivityCheck.setText("Create Activity:");
mCreateActivityCheck.setToolTipText("Specifies if you want to create a default Activity.");
mCreateActivityCheck.setFont(parent.getFont());
mCreateActivityCheck.setSelection(INITIAL_CREATE_ACTIVITY);
mCreateActivityCheck.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
onCreateActivityCheckModified();
enableLocationWidgets();
}
});
// new activity name entry field
mActivityNameField = new Text(group, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
mActivityNameField.setToolTipText("Name of the Activity class to create. Must be a valid Java identifier.");
mActivityNameField.setLayoutData(data);
mActivityNameField.setFont(parent.getFont());
mActivityNameField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onActivityNameFieldModified();
}
});
// min sdk version label
label = new Label(group, SWT.NONE);
label.setText("Min SDK Version:");
label.setFont(parent.getFont());
label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
// min sdk version entry field
mMinSdkVersionField = new Text(group, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
mMinSdkVersionField.setLayoutData(data);
mMinSdkVersionField.setFont(parent.getFont());
mMinSdkVersionField.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
onMinSdkVersionFieldModified();
setPageComplete(validatePage());
}
});
}
//--- Internal getters & setters ------------------
/** Returns the location path field value with spaces trimmed. */
private String getLocationPathFieldValue() {
return mLocationPathField == null ? "" : mLocationPathField.getText().trim();
}
/** Returns the current project location, depending on the Use Default Location check box. */
public String getProjectLocation() {
if (isNewProject() && useDefaultLocation()) {
return Platform.getLocation().toString();
} else {
return getLocationPathFieldValue();
}
}
/**
* Creates a project resource handle for the current project name field
* value.
* <p>
* This method does not create the project resource; this is the
* responsibility of <code>IProject::create</code> invoked by the new
* project resource wizard.
* </p>
*
* @return the new project resource handle
*/
private IProject getProjectHandle() {
return ResourcesPlugin.getWorkspace().getRoot().getProject(getProjectName());
}
// --- UI Callbacks ----
/**
* Enables or disable the location widgets depending on the user selection:
* the location path is enabled when using the "existing source" mode (i.e. not new project)
* or in new project mode with the "use default location" turned off.
*/
private void enableLocationWidgets() {
boolean is_new_project = isNewProject();
boolean use_default = useDefaultLocation();
boolean location_enabled = !is_new_project || !use_default;
boolean create_activity = isCreateActivity();
mUseDefaultLocation.setEnabled(is_new_project);
mLocationLabel.setEnabled(location_enabled);
mLocationPathField.setEnabled(location_enabled);
mBrowseButton.setEnabled(location_enabled);
mPackageNameField.setEnabled(is_new_project);
mCreateActivityCheck.setEnabled(is_new_project);
mActivityNameField.setEnabled(is_new_project & create_activity);
updateLocationPathField(null);
updatePackageAndActivityFields();
}
/**
* Updates the location directory path field.
* <br/>
* When custom user selection is enabled, use the abs_dir argument if not null and also
* save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the
* user selection to be remembered when the user switches from default to custom.
* <br/>
* When custom user selection is disabled, use the workspace default location with the
* current project name. This does not change the internally cached abs_dir.
*
* @param abs_dir A new absolute directory path or null to use the default.
*/
private void updateLocationPathField(String abs_dir) {
boolean is_new_project = isNewProject();
boolean use_default = useDefaultLocation();
boolean custom_location = !is_new_project || !use_default;
if (!mInternalLocationPathUpdate) {
mInternalLocationPathUpdate = true;
if (custom_location) {
if (abs_dir != null) {
// We get here if the user selected a directory with the "Browse" button.
// Disable auto-compute of the custom location unless the user selected
// the exact same path.
sAutoComputeCustomLocation = sAutoComputeCustomLocation &&
abs_dir.equals(sCustomLocationOsPath);
sCustomLocationOsPath = TextProcessor.process(abs_dir);
} else if (sAutoComputeCustomLocation ||
(!is_new_project && !new File(sCustomLocationOsPath).isDirectory())) {
// By default select the samples directory of the current target
IAndroidTarget target = getSdkTarget();
if (target != null) {
sCustomLocationOsPath = target.getPath(IAndroidTarget.SAMPLES);
}
// If we don't have a target, select the base directory of the
// "universal sdk". If we don't even have that, use a root drive.
if (sCustomLocationOsPath == null || sCustomLocationOsPath.length() == 0) {
if (Sdk.getCurrent() != null) {
sCustomLocationOsPath = Sdk.getCurrent().getSdkLocation();
} else {
sCustomLocationOsPath = File.listRoots()[0].getAbsolutePath();
}
}
}
if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) {
mLocationPathField.setText(sCustomLocationOsPath);
}
} else {
String value = Platform.getLocation().append(getProjectName()).toString();
value = TextProcessor.process(value);
if (!mLocationPathField.getText().equals(value)) {
mLocationPathField.setText(value);
}
}
setPageComplete(validatePage());
mInternalLocationPathUpdate = false;
}
}
/**
* The location path field is either modified internally (from updateLocationPathField)
* or manually by the user when the custom_location mode is not set.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onLocationPathFieldModified() {
if (!mInternalLocationPathUpdate) {
// When the updates doesn't come from updateLocationPathField, it must be the user
// editing the field manually, in which case we want to save the value internally
// and we disable auto-compute of the custom location (to avoid overriding the user
// value)
String newPath = getLocationPathFieldValue();
sAutoComputeCustomLocation = sAutoComputeCustomLocation &&
newPath.equals(sCustomLocationOsPath);
sCustomLocationOsPath = newPath;
extractNamesFromAndroidManifest();
setPageComplete(validatePage());
}
}
/**
* The package name field is either modified internally (from extractNamesFromAndroidManifest)
* or manually by the user when the custom_location mode is not set.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onPackageNameFieldModified() {
if (isNewProject()) {
mUserPackageName = getPackageName();
setPageComplete(validatePage());
}
}
/**
* The create activity checkbox is either modified internally (from
* extractNamesFromAndroidManifest) or manually by the user.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onCreateActivityCheckModified() {
if (isNewProject() && !mInternalCreateActivityUpdate) {
mUserCreateActivityCheck = isCreateActivity();
}
setPageComplete(validatePage());
}
/**
* The activity name field is either modified internally (from extractNamesFromAndroidManifest)
* or manually by the user when the custom_location mode is not set.
*
* Ignore the internal modification. When modified by the user, memorize the choice and
* validate the page.
*/
private void onActivityNameFieldModified() {
if (isNewProject() && !mInternalActivityNameUpdate) {
mUserActivityName = getActivityName();
setPageComplete(validatePage());
}
}
/**
* Called when the min sdk version field has been modified.
*
* Ignore the internal modifications. When modified by the user, try to match
* a target with the same API level.
*/
private void onMinSdkVersionFieldModified() {
if (mInternalMinSdkVersionUpdate) {
return;
}
try {
int version = Integer.parseInt(getMinSdkVersion());
// Before changing, compare with the currently selected one, if any.
// There can be multiple targets with the same sdk api version, so don't change
// it if it's already at the right version.
IAndroidTarget curr_target = getSdkTarget();
if (curr_target != null && curr_target.getApiVersionNumber() == version) {
return;
}
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (target.getApiVersionNumber() == version) {
mSdkTargetSelector.setSelection(target);
break;
}
}
} catch (NumberFormatException e) {
// ignore
}
mMinSdkVersionModifiedByUser = true;
}
/**
* Called when an SDK target is modified.
*
* If the minSdkVersion field hasn't been modified by the user yet, we change it
* to reflect the sdk api level that has just been selected.
*/
private void onSdkTargetModified() {
IAndroidTarget target = getSdkTarget();
if (target != null && !mMinSdkVersionModifiedByUser) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(Integer.toString(target.getApiVersionNumber()));
mInternalMinSdkVersionUpdate = false;
}
}
/**
* Called when the radio buttons are changed between the "create new project" and the
* "use existing source" mode. This reverts the fields to whatever the user manually
* entered before.
*/
private void updatePackageAndActivityFields() {
if (isNewProject()) {
if (mUserPackageName.length() > 0 &&
!mPackageNameField.getText().equals(mUserPackageName)) {
mPackageNameField.setText(mUserPackageName);
}
if (mUserActivityName.length() > 0 &&
!mActivityNameField.getText().equals(mUserActivityName)) {
mInternalActivityNameUpdate = true;
mActivityNameField.setText(mUserActivityName);
mInternalActivityNameUpdate = false;
}
if (mUserCreateActivityCheck != mCreateActivityCheck.getSelection()) {
mInternalCreateActivityUpdate = true;
mCreateActivityCheck.setSelection(mUserCreateActivityCheck);
mInternalCreateActivityUpdate = false;
}
}
}
/**
* Extract names from an android manifest.
* This is done only if the user selected the "use existing source" and a manifest xml file
* can actually be found in the custom user directory.
*/
private void extractNamesFromAndroidManifest() {
if (isNewProject()) {
return;
}
String projectLocation = getProjectLocation();
File f = new File(projectLocation);
if (!f.isDirectory()) {
return;
}
Path path = new Path(f.getPath());
String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString();
AndroidManifestParser manifestData = AndroidManifestParser.parseForData(osPath);
if (manifestData == null) {
return;
}
String packageName = null;
Activity activity = null;
String activityName = null;
int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
try {
packageName = manifestData.getPackage();
minSdkVersion = manifestData.getApiLevelRequirement();
// try to get the first launcher activity. If none, just take the first activity.
activity = manifestData.getLauncherActivity();
if (activity == null) {
Activity[] activities = manifestData.getActivities();
if (activities != null && activities.length > 0) {
activity = activities[0];
}
}
} catch (Exception e) {
// ignore exceptions
}
if (packageName != null && packageName.length() > 0) {
mPackageNameField.setText(packageName);
}
if (activity != null) {
activityName = AndroidManifestParser.extractActivityName(activity.getName(),
packageName);
}
if (activityName != null && activityName.length() > 0) {
mInternalActivityNameUpdate = true;
mInternalCreateActivityUpdate = true;
mActivityNameField.setText(activityName);
mCreateActivityCheck.setSelection(true);
mInternalCreateActivityUpdate = false;
mInternalActivityNameUpdate = false;
// If project name and application names are empty, use the activity
// name as a default. If the activity name has dots, it's a part of a
// package specification and only the last identifier must be used.
if (activityName.indexOf('.') != -1) {
String[] ids = activityName.split(AndroidConstants.RE_DOT);
activityName = ids[ids.length - 1];
}
if (mProjectNameField.getText().length() == 0 ||
!mProjectNameModifiedByUser) {
mInternalProjectNameUpdate = true;
mProjectNameField.setText(activityName);
mInternalProjectNameUpdate = false;
}
if (mApplicationNameField.getText().length() == 0 ||
!mApplicationNameModifiedByUser) {
mInternalApplicationNameUpdate = true;
mApplicationNameField.setText(activityName);
mInternalApplicationNameUpdate = false;
}
} else {
mInternalActivityNameUpdate = true;
mInternalCreateActivityUpdate = true;
mActivityNameField.setText(""); //$NON-NLS-1$
mCreateActivityCheck.setSelection(false);
mInternalCreateActivityUpdate = false;
mInternalActivityNameUpdate = false;
// There is no activity name to use to fill in the project and application
// name. However if there's a package name, we can use this as a base.
if (packageName != null && packageName.length() > 0) {
// Package name is a java identifier, so it's most suitable for
// an application name.
if (mApplicationNameField.getText().length() == 0 ||
!mApplicationNameModifiedByUser) {
mInternalApplicationNameUpdate = true;
mApplicationNameField.setText(packageName);
mInternalApplicationNameUpdate = false;
}
// For the project name, remove any dots
packageName = packageName.replace('.', '_');
if (mProjectNameField.getText().length() == 0 ||
!mProjectNameModifiedByUser) {
mInternalProjectNameUpdate = true;
mProjectNameField.setText(packageName);
mInternalProjectNameUpdate = false;
}
}
}
// Select the target matching the manifest's sdk or build properties, if any
boolean foundTarget = false;
ProjectProperties p = ProjectProperties.create(projectLocation, null);
if (p != null) {
// Check the {build|default}.properties files if present
p.merge(PropertyType.BUILD).merge(PropertyType.DEFAULT);
String v = p.getProperty(ProjectProperties.PROPERTY_TARGET);
IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(v);
if (target != null) {
mSdkTargetSelector.setSelection(target);
foundTarget = true;
}
}
if (!foundTarget && minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK) {
try {
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (target.getApiVersionNumber() == minSdkVersion) {
mSdkTargetSelector.setSelection(target);
foundTarget = true;
break;
}
}
} catch(NumberFormatException e) {
// ignore
}
}
if (!foundTarget) {
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (projectLocation.startsWith(target.getLocation())) {
mSdkTargetSelector.setSelection(target);
foundTarget = true;
break;
}
}
}
if (!foundTarget) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(
minSdkVersion == AndroidManifestParser.INVALID_MIN_SDK ? "" :
Integer.toString(minSdkVersion)); //$NON-NLS-1$
mInternalMinSdkVersionUpdate = false;
}
}
/**
* Returns whether this page's controls currently all contain valid values.
*
* @return <code>true</code> if all controls are valid, and
* <code>false</code> if at least one is invalid
*/
protected boolean validatePage() {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
int status = validateProjectField(workspace);
if ((status & MSG_ERROR) == 0) {
status |= validateLocationPath(workspace);
}
if ((status & MSG_ERROR) == 0) {
status |= validateSdkTarget();
}
if ((status & MSG_ERROR) == 0) {
status |= validatePackageField();
}
if ((status & MSG_ERROR) == 0) {
status |= validateActivityField();
}
if ((status & MSG_ERROR) == 0) {
status |= validateMinSdkVersionField();
}
if ((status & MSG_ERROR) == 0) {
status |= validateSourceFolder();
}
if (status == MSG_NONE) {
setStatus(null, MSG_NONE);
}
// Return false if there's an error so that the finish button be disabled.
return (status & MSG_ERROR) == 0;
}
/**
* Validates the project name field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateProjectField(IWorkspace workspace) {
// Validate project field
String projectFieldContents = getProjectName();
if (projectFieldContents.length() == 0) {
return setStatus("Project name must be specified", MSG_ERROR);
}
// Limit the project name to shell-agnostic characters since it will be used to
// generate the final package
if (!sProjectNamePattern.matcher(projectFieldContents).matches()) {
return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.",
MSG_ERROR);
}
IStatus nameStatus = workspace.validateName(projectFieldContents, IResource.PROJECT);
if (!nameStatus.isOK()) {
return setStatus(nameStatus.getMessage(), MSG_ERROR);
}
if (getProjectHandle().exists()) {
return setStatus("A project with that name already exists in the workspace",
MSG_ERROR);
}
return MSG_NONE;
}
/**
* Validates the location path field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateLocationPath(IWorkspace workspace) {
Path path = new Path(getProjectLocation());
if (isNewProject()) {
if (!useDefaultLocation()) {
// If not using the default value validate the location.
URI uri = URIUtil.toURI(path.toOSString());
IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(),
uri);
if (!locationStatus.isOK()) {
return setStatus(locationStatus.getMessage(), MSG_ERROR);
} else {
// The location is valid as far as Eclipse is concerned (i.e. mostly not
// an existing workspace project.) Check it either doesn't exist or is
// a directory that is empty.
File f = path.toFile();
if (f.exists() && !f.isDirectory()) {
return setStatus("A directory name must be specified.", MSG_ERROR);
} else if (f.isDirectory()) {
// However if the directory exists, we should put a warning if it is not
// empty. We don't put an error (we'll ask the user again for confirmation
// before using the directory.)
String[] l = f.list();
if (l.length != 0) {
return setStatus("The selected output directory is not empty.",
MSG_WARNING);
}
}
}
} else {
// Otherwise validate the path string is not empty
if (getProjectLocation().length() == 0) {
return setStatus("A directory name must be specified.", MSG_ERROR);
}
File dest = path.append(getProjectName()).toFile();
if (dest.exists()) {
return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.",
getProjectName()), MSG_ERROR);
}
}
} else {
// Must be an existing directory
File f = path.toFile();
if (!f.isDirectory()) {
return setStatus("An existing directory name must be specified.", MSG_ERROR);
}
// Check there's an android manifest in the directory
String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString();
File manifestFile = new File(osPath);
if (!manifestFile.isFile()) {
return setStatus(
String.format("File %1$s not found in %2$s.",
AndroidConstants.FN_ANDROID_MANIFEST, f.getName()),
MSG_ERROR);
}
// Parse it and check the important fields.
AndroidManifestParser manifestData = AndroidManifestParser.parseForData(osPath);
if (manifestData == null) {
return setStatus(
String.format("File %1$s could not be parsed.", osPath),
MSG_ERROR);
}
String packageName = manifestData.getPackage();
if (packageName == null || packageName.length() == 0) {
return setStatus(
String.format("No package name defined in %1$s.", osPath),
MSG_ERROR);
}
Activity[] activities = manifestData.getActivities();
if (activities == null || activities.length == 0) {
// This is acceptable now as long as no activity needs to be created
if (isCreateActivity()) {
return setStatus(
String.format("No activity name defined in %1$s.", osPath),
MSG_ERROR);
}
}
// If there's already a .project, tell the user to use import instead.
if (path.append(".project").toFile().exists()) { //$NON-NLS-1$
return setStatus("An Eclipse project already exists in this directory. Consider using File > Import > Existing Project instead.",
MSG_WARNING);
}
}
return MSG_NONE;
}
/**
* Validates the sdk target choice.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateSdkTarget() {
if (getSdkTarget() == null) {
return setStatus("An SDK Target must be specified.", MSG_ERROR);
}
return MSG_NONE;
}
/**
* Validates the sdk target choice.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateMinSdkVersionField() {
// If the min sdk version is empty, it is always accepted.
if (getMinSdkVersion().length() == 0) {
return MSG_NONE;
}
int version = AndroidManifestParser.INVALID_MIN_SDK;
try {
// If not empty, it must be a valid integer > 0
version = Integer.parseInt(getMinSdkVersion());
} catch (NumberFormatException e) {
// ignore
}
if (version < 1) {
return setStatus("Min SDK Version must be an integer > 0.", MSG_ERROR);
}
if (getSdkTarget() != null && getSdkTarget().getApiVersionNumber() != version) {
return setStatus("The API level for the selected SDK target does not match the Min SDK version.",
MSG_WARNING);
}
return MSG_NONE;
}
/**
* Validates the activity name field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateActivityField() {
// Disregard if not creating an activity
if (!isCreateActivity()) {
return MSG_NONE;
}
// Validate activity field
String activityFieldContents = getActivityName();
if (activityFieldContents.length() == 0) {
return setStatus("Activity name must be specified.", MSG_ERROR);
}
// The activity field can actually contain part of a sub-package name
// or it can start with a dot "." to indicates it comes from the parent package name.
String packageName = "";
int pos = activityFieldContents.lastIndexOf('.');
if (pos >= 0) {
packageName = activityFieldContents.substring(0, pos);
if (packageName.startsWith(".")) { //$NON-NLS-1$
packageName = packageName.substring(1);
}
activityFieldContents = activityFieldContents.substring(pos + 1);
}
// the activity field can contain a simple java identifier, or a
// package name or one that starts with a dot. So if it starts with a dot,
// ignore this dot -- the rest must look like a package name.
if (activityFieldContents.charAt(0) == '.') {
activityFieldContents = activityFieldContents.substring(1);
}
// Check it's a valid activity string
int result = MSG_NONE;
IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents,
"1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
if (!status.isOK()) {
result = setStatus(status.getMessage(),
status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
}
// Check it's a valid package string
if (result != MSG_ERROR && packageName.length() > 0) {
status = JavaConventions.validatePackageName(packageName,
"1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
if (!status.isOK()) {
result = setStatus(status.getMessage() + " (in the activity name)",
status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
}
}
return result;
}
/**
* Validates the package name field.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validatePackageField() {
// Validate package field
String packageFieldContents = getPackageName();
if (packageFieldContents.length() == 0) {
return setStatus("Package name must be specified.", MSG_ERROR);
}
// Check it's a valid package string
int result = MSG_NONE;
IStatus status = JavaConventions.validatePackageName(packageFieldContents, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
if (!status.isOK()) {
result = setStatus(status.getMessage(),
status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
}
// The Android Activity Manager does not accept packages names with only one
// identifier. Check the package name has at least one dot in them (the previous rule
// validated that if such a dot exist, it's not the first nor last characters of the
// string.)
if (result != MSG_ERROR && packageFieldContents.indexOf('.') == -1) {
return setStatus("Package name must have at least two identifiers.", MSG_ERROR);
}
return result;
}
/**
* Validates that an existing project actually has a source folder.
*
* For project in "use existing source" mode, this tries to find the source folder.
* A source folder should be just under the project directory and it should have all
* the directories composing the package+activity name.
*
* As a side effect, it memorizes the source folder in mSourceFolder.
*
* TODO: support multiple source folders for multiple activities.
*
* @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
*/
private int validateSourceFolder() {
// This check does nothing when creating a new project.
// This check is also useless when no activity is present or created.
if (isNewProject() || !isCreateActivity()) {
return MSG_NONE;
}
String osTarget = getActivityName();
if (osTarget.indexOf('.') == -1) {
osTarget = getPackageName() + File.separator + osTarget;
} else if (osTarget.indexOf('.') == 0) {
osTarget = getPackageName() + osTarget;
}
osTarget = osTarget.replace('.', File.separatorChar) + AndroidConstants.DOT_JAVA;
String projectPath = getProjectLocation();
File projectDir = new File(projectPath);
File[] all_dirs = projectDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
for (File f : all_dirs) {
Path path = new Path(f.getAbsolutePath());
File java_activity = path.append(osTarget).toFile();
if (java_activity.isFile()) {
mSourceFolder = f.getName();
return MSG_NONE;
}
}
if (all_dirs.length > 0) {
return setStatus(
String.format("%1$s can not be found under %2$s.", osTarget, projectPath),
MSG_ERROR);
} else {
return setStatus(
String.format("No source folders can be found in %1$s.", projectPath),
MSG_ERROR);
}
}
/**
* Sets the error message for the wizard with the given message icon.
*
* @param message The wizard message type, one of MSG_ERROR or MSG_WARNING.
* @return As a convenience, always returns messageType so that the caller can return
* immediately.
*/
private int setStatus(String message, int messageType) {
if (message == null) {
setErrorMessage(null);
setMessage(null);
} else if (!message.equals(getMessage())) {
setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR);
}
return messageType;
}
}