aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java85
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java36
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java36
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java183
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java626
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java378
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java339
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java268
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java264
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java291
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java642
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java275
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties27
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java56
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java112
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java111
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java90
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java275
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java425
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java809
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java512
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java256
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java1520
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java181
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java412
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java32
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java32
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java606
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java271
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java487
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java293
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java109
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java130
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java653
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java282
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java1163
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java431
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java326
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java107
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java61
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java298
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java187
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java931
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java456
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java125
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java946
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java212
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java417
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java380
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java101
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java1239
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java261
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java468
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java161
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java78
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java222
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java94
75 files changed, 20974 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java
new file mode 100644
index 000000000..4d3870b86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
+import com.android.ide.eclipse.adt.internal.project.ExportHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbenchPart;
+
+public class ExportAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+ private Shell mShell;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ @Override
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ mShell = targetPart.getSite().getShell();
+ }
+
+ @Override
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection)mSelection;
+ // get the unique selected item.
+ if (selection.size() == 1) {
+ Object element = selection.getFirstElement();
+
+ // get the project object from it.
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject) element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
+ }
+
+ // and finally do the action
+ if (project != null) {
+ if (!EclipseLintRunner.runLintOnExport(mShell, project)) {
+ return;
+ }
+
+ ProjectState state = Sdk.getProjectState(project);
+ if (state.isLibrary()) {
+ MessageDialog.openError(mShell, "Android Export",
+ "Android library projects cannot be exported.");
+ } else {
+ ExportHelper.exportUnsignedReleaseApk(project);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ mSelection = selection;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java
new file mode 100644
index 000000000..673d9569f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPart;
+
+public class ExportWizardAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+ private IWorkbench mWorkbench;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ @Override
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench();
+ }
+
+ @Override
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection)mSelection;
+
+ // get the unique selected item.
+ if (selection.size() == 1) {
+ Object element = selection.getFirstElement();
+
+ // get the project object from it.
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject) element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
+ }
+
+ // and finally do the action
+ if (project != null) {
+ if (!EclipseLintRunner.runLintOnExport(
+ mWorkbench.getActiveWorkbenchWindow().getShell(), project)) {
+ return;
+ }
+
+ ProjectState state = Sdk.getProjectState(project);
+ if (state.isLibrary()) {
+ MessageDialog.openError(mWorkbench.getDisplay().getActiveShell(),
+ "Android Export",
+ "Android library projects cannot be exported.");
+ } else {
+ // call the export wizard on the current selection.
+ ExportWizard wizard = new ExportWizard();
+ wizard.init(mWorkbench, selection);
+ WizardDialog dialog = new WizardDialog(
+ mWorkbench.getDisplay().getActiveShell(), wizard);
+ dialog.open();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ mSelection = selection;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java
new file mode 100644
index 000000000..38f4768b1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IWorkbenchWizard;
+
+/**
+ * Delegate for the toolbar action "Android Project".
+ * It displays the Android New Project wizard to create a new Android Project (not a test project).
+ *
+ * @see NewTestProjectAction
+ */
+public class NewProjectAction extends OpenWizardAction {
+
+ @Override
+ protected IWorkbenchWizard instanciateWizard(IAction action) {
+ return new NewProjectWizard();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java
new file mode 100755
index 000000000..c8e45ef1a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IWorkbenchWizard;
+
+/**
+ * Delegate for the toolbar action "Android Test Project".
+ * It displays the Android New Project wizard to create a new Test Project.
+ */
+public class NewTestProjectAction extends OpenWizardAction {
+
+ @Override
+ protected IWorkbenchWizard instanciateWizard(IAction action) {
+ return new NewTestProjectWizard();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java
new file mode 100644
index 000000000..ba349c30a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IWorkbenchWizard;
+
+/**
+ * Delegate for the toolbar action "Android Project" or for the
+ * project > Android Project context menu.
+ *
+ * It displays the Android New XML file wizard.
+ */
+public class NewXmlFileAction extends OpenWizardAction {
+
+ @Override
+ protected IWorkbenchWizard instanciateWizard(IAction action) {
+ return new NewXmlFileWizard();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java
new file mode 100644
index 000000000..a3e6135e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.actions;
+
+import com.android.ide.eclipse.adt.internal.ui.IUpdateWizardDialog;
+import com.android.ide.eclipse.adt.internal.ui.WizardDialogEx;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.IWorkbenchWizard;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.internal.IWorkbenchHelpContextIds;
+import org.eclipse.ui.internal.LegacyResourceSupport;
+import org.eclipse.ui.internal.actions.NewWizardShortcutAction;
+import org.eclipse.ui.internal.util.Util;
+
+/**
+ * An abstract action that displays one of our wizards.
+ * Derived classes must provide the actual wizard to display.
+ */
+/*package*/ abstract class OpenWizardAction
+ implements IWorkbenchWindowActionDelegate, IObjectActionDelegate {
+
+ /**
+ * The wizard dialog width, extracted from {@link NewWizardShortcutAction}
+ */
+ private static final int SIZING_WIZARD_WIDTH = 500;
+
+ /**
+ * The wizard dialog height, extracted from {@link NewWizardShortcutAction}
+ */
+ private static final int SIZING_WIZARD_HEIGHT = 500;
+
+ /** The wizard that was created by {@link #run(IAction)}. */
+ private IWorkbenchWizard mWizard;
+ /** The result from the dialog */
+ private int mDialogResult;
+
+ private ISelection mSelection;
+ private IWorkbench mWorkbench;
+
+ /** Returns the wizard that was created by {@link #run(IAction)}. */
+ public IWorkbenchWizard getWizard() {
+ return mWizard;
+ }
+
+ /** Returns the result from {@link Dialog#open()}, available after
+ * the completion of {@link #run(IAction)}. */
+ public int getDialogResult() {
+ return mDialogResult;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose()
+ */
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow)
+ */
+ @Override
+ public void init(IWorkbenchWindow window) {
+ // pass
+ }
+
+ /**
+ * Opens and display the Android New Project Wizard.
+ * <p/>
+ * Most of this implementation is extracted from {@link NewWizardShortcutAction#run()}.
+ *
+ * @param action The action that got us here. Can be null when used internally.
+ * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
+ */
+ @Override
+ public void run(IAction action) {
+
+ // get the workbench and the current window
+ IWorkbench workbench = mWorkbench != null ? mWorkbench : PlatformUI.getWorkbench();
+ IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
+
+ // This code from NewWizardShortcutAction#run() gets the current window selection
+ // and converts it to a workbench structured selection for the wizard, if possible.
+ ISelection selection = mSelection;
+ if (selection == null) {
+ selection = window.getSelectionService().getSelection();
+ }
+
+ IStructuredSelection selectionToPass = StructuredSelection.EMPTY;
+ if (selection instanceof IStructuredSelection) {
+ selectionToPass = (IStructuredSelection) selection;
+ } else {
+ // Build the selection from the IFile of the editor
+ IWorkbenchPart part = window.getPartService().getActivePart();
+ if (part instanceof IEditorPart) {
+ IEditorInput input = ((IEditorPart) part).getEditorInput();
+ Class<?> fileClass = LegacyResourceSupport.getFileClass();
+ if (input != null && fileClass != null) {
+ Object file = Util.getAdapter(input, fileClass);
+ if (file != null) {
+ selectionToPass = new StructuredSelection(file);
+ }
+ }
+ }
+ }
+
+ // Create the wizard and initialize it with the selection
+ mWizard = instanciateWizard(action);
+ mWizard.init(workbench, selectionToPass);
+
+ // It's not visible yet until a dialog is created and opened
+ Shell parent = window.getShell();
+ WizardDialogEx dialog = new WizardDialogEx(parent, mWizard);
+ dialog.create();
+
+ if (mWizard instanceof IUpdateWizardDialog) {
+ ((IUpdateWizardDialog) mWizard).updateWizardDialog(dialog);
+ }
+
+ // This code comes straight from NewWizardShortcutAction#run()
+ Point defaultSize = dialog.getShell().getSize();
+ dialog.getShell().setSize(
+ Math.max(SIZING_WIZARD_WIDTH, defaultSize.x),
+ Math.max(SIZING_WIZARD_HEIGHT, defaultSize.y));
+ window.getWorkbench().getHelpSystem().setHelp(dialog.getShell(),
+ IWorkbenchHelpContextIds.NEW_WIZARD_SHORTCUT);
+
+ mDialogResult = dialog.open();
+ }
+
+ /**
+ * Called by {@link #run(IAction)} to instantiate the actual wizard.
+ *
+ * @param action The action parameter from {@link #run(IAction)}.
+ * This can be null.
+ * @return A new wizard instance. Must not be null.
+ */
+ protected abstract IWorkbenchWizard instanciateWizard(IAction action);
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection)
+ */
+ @Override
+ public void selectionChanged(IAction action, ISelection selection) {
+ mSelection = selection;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IObjectActionDelegate#setActivePart(org.eclipse.jface.action.IAction, org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java
new file mode 100644
index 000000000..170da6d33
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.ExportHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.BuildToolInfo.PathId;
+import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
+import com.android.sdklib.internal.build.KeystoreHelper;
+import com.android.utils.GrabProcessOutput;
+import com.android.utils.GrabProcessOutput.IProcessOutput;
+import com.android.utils.GrabProcessOutput.Wait;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IExportWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Export wizard to export an apk signed with a release key/certificate.
+ */
+public final class ExportWizard extends Wizard implements IExportWizard {
+
+ private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$
+
+ private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$
+ private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$
+ private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$
+ private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$
+ private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$
+
+ static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$
+ static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$
+ static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$
+
+ static final int APK_FILE_SOURCE = 0;
+ static final int APK_FILE_DEST = 1;
+ static final int APK_COUNT = 2;
+
+ /**
+ * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback.
+ */
+ static abstract class ExportWizardPage extends WizardPage {
+
+ /** bit mask constant for project data change event */
+ protected static final int DATA_PROJECT = 0x001;
+ /** bit mask constant for keystore data change event */
+ protected static final int DATA_KEYSTORE = 0x002;
+ /** bit mask constant for key data change event */
+ protected static final int DATA_KEY = 0x004;
+
+ protected static final VerifyListener sPasswordVerifier = new VerifyListener() {
+ @Override
+ public void verifyText(VerifyEvent e) {
+ // verify the characters are valid for password.
+ int len = e.text.length();
+
+ // first limit to 127 characters max
+ if (len + ((Text)e.getSource()).getText().length() > 127) {
+ e.doit = false;
+ return;
+ }
+
+ // now only take non control characters
+ for (int i = 0 ; i < len ; i++) {
+ if (e.text.charAt(i) < 32) {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ };
+
+ /**
+ * Bit mask indicating what changed while the page was hidden.
+ * @see #DATA_PROJECT
+ * @see #DATA_KEYSTORE
+ * @see #DATA_KEY
+ */
+ protected int mProjectDataChanged = 0;
+
+ ExportWizardPage(String name) {
+ super(name);
+ }
+
+ abstract void onShow();
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ onShow();
+ mProjectDataChanged = 0;
+ }
+ }
+
+ final void projectDataChanged(int changeMask) {
+ mProjectDataChanged |= changeMask;
+ }
+
+ /**
+ * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a
+ * {@link Throwable} object.
+ */
+ protected void onException(Throwable t) {
+ String message = getExceptionMessage(t);
+
+ setErrorMessage(message);
+ setPageComplete(false);
+ }
+ }
+
+ private ExportWizardPage mPages[] = new ExportWizardPage[5];
+
+ private IProject mProject;
+
+ private String mKeystore;
+ private String mKeystorePassword;
+ private boolean mKeystoreCreationMode;
+
+ private String mKeyAlias;
+ private String mKeyPassword;
+ private int mValidity;
+ private String mDName;
+
+ private PrivateKey mPrivateKey;
+ private X509Certificate mCertificate;
+
+ private File mDestinationFile;
+
+ private ExportWizardPage mKeystoreSelectionPage;
+ private ExportWizardPage mKeyCreationPage;
+ private ExportWizardPage mKeySelectionPage;
+ private ExportWizardPage mKeyCheckPage;
+
+ private boolean mKeyCreationMode;
+
+ private List<String> mExistingAliases;
+
+ public ExportWizard() {
+ setHelpAvailable(false); // TODO have help
+ setWindowTitle("Export Android Application");
+ setImageDescriptor();
+ }
+
+ @Override
+ public void addPages() {
+ addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK));
+ addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this,
+ PAGE_KEYSTORE_SELECTION));
+ addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION));
+ addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION));
+ addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK));
+ }
+
+ @Override
+ public boolean performFinish() {
+ // save the properties
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore);
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias);
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION,
+ mDestinationFile.getAbsolutePath());
+
+ // run the export in an UI runnable.
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ final boolean[] result = new boolean[1];
+ try {
+ workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
+ /**
+ * Run the export.
+ * @throws InvocationTargetException
+ * @throws InterruptedException
+ */
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException,
+ InterruptedException {
+ try {
+ result[0] = doExport(monitor);
+ } finally {
+ monitor.done();
+ }
+ }
+ });
+ } catch (InvocationTargetException e) {
+ return false;
+ } catch (InterruptedException e) {
+ return false;
+ }
+
+ return result[0];
+ }
+
+ private boolean doExport(IProgressMonitor monitor) {
+ try {
+ // if needed, create the keystore and/or key.
+ if (mKeystoreCreationMode || mKeyCreationMode) {
+ final ArrayList<String> output = new ArrayList<String>();
+ boolean createdStore = KeystoreHelper.createNewStore(
+ mKeystore,
+ null /*storeType*/,
+ mKeystorePassword,
+ mKeyAlias,
+ mKeyPassword,
+ mDName,
+ mValidity,
+ new IKeyGenOutput() {
+ @Override
+ public void err(String message) {
+ output.add(message);
+ }
+ @Override
+ public void out(String message) {
+ output.add(message);
+ }
+ });
+
+ if (createdStore == false) {
+ // keystore creation error!
+ displayError(output.toArray(new String[output.size()]));
+ return false;
+ }
+
+ // keystore is created, now load the private key and certificate.
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mKeystore);
+ keyStore.load(fis, mKeystorePassword.toCharArray());
+ fis.close();
+ PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray()));
+
+ if (entry != null) {
+ mPrivateKey = entry.getPrivateKey();
+ mCertificate = (X509Certificate)entry.getCertificate();
+
+ AdtPlugin.printToConsole(mProject,
+ String.format("New keystore %s has been created.",
+ mDestinationFile.getAbsolutePath()),
+ "Certificate fingerprints:",
+ String.format(" MD5 : %s", getCertMd5Fingerprint()),
+ String.format(" SHA1: %s", getCertSha1Fingerprint()));
+
+ } else {
+ // this really shouldn't happen since we now let the user choose the key
+ // from a list read from the store.
+ displayError("Could not find key");
+ return false;
+ }
+ }
+
+ // check the private key/certificate again since it may have been created just above.
+ if (mPrivateKey != null && mCertificate != null) {
+ // check whether we can run zipalign.
+ boolean runZipAlign = false;
+
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ BuildToolInfo buildToolInfo = ExportHelper.getBuildTools(projectState);
+
+ String zipAlignPath = buildToolInfo.getPath(PathId.ZIP_ALIGN);
+ runZipAlign = zipAlignPath != null && new File(zipAlignPath).isFile();
+
+ File apkExportFile = mDestinationFile;
+ if (runZipAlign) {
+ // create a temp file for the original export.
+ apkExportFile = File.createTempFile("androidExport_", ".apk");
+ }
+
+ // export the signed apk.
+ ExportHelper.exportReleaseApk(mProject, apkExportFile,
+ mPrivateKey, mCertificate, monitor);
+
+ // align if we can
+ if (runZipAlign) {
+ String message = zipAlign(zipAlignPath, apkExportFile, mDestinationFile);
+ if (message != null) {
+ displayError(message);
+ return false;
+ }
+ } else {
+ AdtPlugin.displayWarning("Export Wizard",
+ "The zipalign tool was not found in the SDK.\n\n" +
+ "Please update to the latest SDK and re-export your application\n" +
+ "or run zipalign manually.\n\n" +
+ "Aligning applications allows Android to use application resources\n" +
+ "more efficiently.");
+ }
+
+ return true;
+ }
+ } catch (Throwable t) {
+ displayError(t);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean canFinish() {
+ // check if we have the apk to resign, the destination location, and either
+ // a private key/certificate or the creation mode. In creation mode, unless
+ // all the key/keystore info is valid, the user cannot reach the last page, so there's
+ // no need to check them again here.
+ return ((mPrivateKey != null && mCertificate != null)
+ || mKeystoreCreationMode || mKeyCreationMode) &&
+ mDestinationFile != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench,
+ * org.eclipse.jface.viewers.IStructuredSelection)
+ */
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ // get the project from the selection
+ Object selected = selection.getFirstElement();
+
+ if (selected instanceof IProject) {
+ mProject = (IProject)selected;
+ } else if (selected instanceof IAdaptable) {
+ IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class);
+ if (r != null) {
+ mProject = r.getProject();
+ }
+ }
+ }
+
+ ExportWizardPage getKeystoreSelectionPage() {
+ return mKeystoreSelectionPage;
+ }
+
+ ExportWizardPage getKeyCreationPage() {
+ return mKeyCreationPage;
+ }
+
+ ExportWizardPage getKeySelectionPage() {
+ return mKeySelectionPage;
+ }
+
+ ExportWizardPage getKeyCheckPage() {
+ return mKeyCheckPage;
+ }
+
+ /**
+ * Returns an image descriptor for the wizard logo.
+ */
+ private void setImageDescriptor() {
+ ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+ }
+
+ IProject getProject() {
+ return mProject;
+ }
+
+ void setProject(IProject project) {
+ mProject = project;
+
+ updatePageOnChange(ExportWizardPage.DATA_PROJECT);
+ }
+
+ void setKeystore(String path) {
+ mKeystore = path;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ String getKeystore() {
+ return mKeystore;
+ }
+
+ void setKeystoreCreationMode(boolean createStore) {
+ mKeystoreCreationMode = createStore;
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ boolean getKeystoreCreationMode() {
+ return mKeystoreCreationMode;
+ }
+
+
+ void setKeystorePassword(String password) {
+ mKeystorePassword = password;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ String getKeystorePassword() {
+ return mKeystorePassword;
+ }
+
+ void setKeyCreationMode(boolean createKey) {
+ mKeyCreationMode = createKey;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ boolean getKeyCreationMode() {
+ return mKeyCreationMode;
+ }
+
+ void setExistingAliases(List<String> aliases) {
+ mExistingAliases = aliases;
+ }
+
+ List<String> getExistingAliases() {
+ return mExistingAliases;
+ }
+
+ void setKeyAlias(String name) {
+ mKeyAlias = name;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getKeyAlias() {
+ return mKeyAlias;
+ }
+
+ void setKeyPassword(String password) {
+ mKeyPassword = password;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getKeyPassword() {
+ return mKeyPassword;
+ }
+
+ void setValidity(int validity) {
+ mValidity = validity;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ int getValidity() {
+ return mValidity;
+ }
+
+ void setDName(String dName) {
+ mDName = dName;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getDName() {
+ return mDName;
+ }
+
+ String getCertSha1Fingerprint() {
+ return FingerprintUtils.getFingerprint(mCertificate, "SHA1");
+ }
+
+ String getCertMd5Fingerprint() {
+ return FingerprintUtils.getFingerprint(mCertificate, "MD5");
+ }
+
+ void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ void setDestination(File destinationFile) {
+ mDestinationFile = destinationFile;
+ }
+
+ void resetDestination() {
+ mDestinationFile = null;
+ }
+
+ void updatePageOnChange(int changeMask) {
+ for (ExportWizardPage page : mPages) {
+ page.projectDataChanged(changeMask);
+ }
+ }
+
+ private void displayError(String... messages) {
+ String message = null;
+ if (messages.length == 1) {
+ message = messages[0];
+ } else {
+ StringBuilder sb = new StringBuilder(messages[0]);
+ for (int i = 1; i < messages.length; i++) {
+ sb.append('\n');
+ sb.append(messages[i]);
+ }
+
+ message = sb.toString();
+ }
+
+ AdtPlugin.displayError("Export Wizard", message);
+ }
+
+ private void displayError(Throwable t) {
+ String message = getExceptionMessage(t);
+ displayError(message);
+
+ AdtPlugin.log(t, "Export Wizard Error");
+ }
+
+ /**
+ * Executes zipalign
+ * @param zipAlignPath location of the zipalign too
+ * @param source file to zipalign
+ * @param destination where to write the resulting file
+ * @return null if success, the error otherwise
+ * @throws IOException
+ */
+ private String zipAlign(String zipAlignPath, File source, File destination) throws IOException {
+ // command line: zipaling -f 4 tmp destination
+ String[] command = new String[5];
+ command[0] = zipAlignPath;
+ command[1] = "-f"; //$NON-NLS-1$
+ command[2] = "4"; //$NON-NLS-1$
+ command[3] = source.getAbsolutePath();
+ command[4] = destination.getAbsolutePath();
+
+ Process process = Runtime.getRuntime().exec(command);
+ final ArrayList<String> output = new ArrayList<String>();
+ try {
+ final IProject project = getProject();
+
+ int status = GrabProcessOutput.grabProcessOutput(
+ process,
+ Wait.WAIT_FOR_READERS,
+ new IProcessOutput() {
+ @Override
+ public void out(@Nullable String line) {
+ if (line != null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
+ project, line);
+ }
+ }
+
+ @Override
+ public void err(@Nullable String line) {
+ if (line != null) {
+ output.add(line);
+ }
+ }
+ });
+
+ if (status != 0) {
+ // build a single message from the array list
+ StringBuilder sb = new StringBuilder("Error while running zipalign:");
+ for (String msg : output) {
+ sb.append('\n');
+ sb.append(msg);
+ }
+
+ return sb.toString();
+ }
+ } catch (InterruptedException e) {
+ // ?
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns
+ * <code>null</code>, the method is called again on the cause of the Throwable object.
+ * <p/>If no Throwable in the chain has a valid message, the canonical name of the first
+ * exception is returned.
+ */
+ static String getExceptionMessage(Throwable t) {
+ String message = t.getMessage();
+ if (message == null) {
+ // no error info? get the stack call to display it
+ // At least that'll give us a better bug report.
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ t.printStackTrace(new PrintStream(baos));
+ message = baos.toString();
+ }
+
+ return message;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java
new file mode 100644
index 000000000..c17f43e38
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+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.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.widgets.FormText;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+
+/**
+ * Final page of the wizard that checks the key and ask for the ouput location.
+ */
+final class KeyCheckPage extends ExportWizardPage {
+
+ private static final int REQUIRED_YEARS = 25;
+
+ private static final String VALIDITY_WARNING =
+ "<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"
+ + "<p>If the certificate expires, you will be forced to sign your application with "
+ + "a different one.</p>"
+ + "<p>Applications cannot be upgraded if their certificate changes from "
+ + "one version to another, forcing a full uninstall/install, which will make "
+ + "the user lose his/her data.</p>"
+ + "<p>Google Play(Android Market) currently requires certificates to be valid "
+ + "until 2033.</p>";
+
+ private final ExportWizard mWizard;
+ private PrivateKey mPrivateKey;
+ private X509Certificate mCertificate;
+ private Text mDestination;
+ private boolean mFatalSigningError;
+ private FormText mDetailText;
+ private ScrolledComposite mScrolledComposite;
+
+ private String mKeyDetails;
+ private String mDestinationDetails;
+
+ protected KeyCheckPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Destination and key/certificate checks");
+ setDescription(""); // TODO
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ setErrorMessage(null);
+ setMessage(null);
+
+ // build the ui.
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ gl.verticalSpacing *= 3;
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ new Label(composite, SWT.NONE).setText("Destination APK file:");
+ mDestination = new Text(composite, SWT.BORDER);
+ mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mDestination.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onDestinationChange(false /*forceDetailUpdate*/);
+ }
+ });
+ final Button browseButton = new Button(composite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Destination file name");
+ // get a default apk name based on the project
+ String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
+ null /*config*/);
+ fileDialog.setFileName(filename);
+
+ String saveLocation = fileDialog.open();
+ if (saveLocation != null) {
+ mDestination.setText(saveLocation);
+ }
+ }
+ });
+
+ mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL);
+ mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 3;
+ mScrolledComposite.setExpandHorizontal(true);
+ mScrolledComposite.setExpandVertical(true);
+
+ mDetailText = new FormText(mScrolledComposite, SWT.NONE);
+ mScrolledComposite.setContent(mDetailText);
+
+ mScrolledComposite.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ updateScrolling();
+ }
+ });
+
+ setControl(composite);
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & DATA_PROJECT) != 0) {
+ // reset the destination from the content of the project
+ IProject project = mWizard.getProject();
+
+ String destination = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_DESTINATION);
+ if (destination != null) {
+ mDestination.setText(destination);
+ }
+ }
+
+ // if anything change we basically reload the data.
+ if (mProjectDataChanged != 0) {
+ mFatalSigningError = false;
+
+ // reset the wizard with no key/cert to make it not finishable, unless a valid
+ // key/cert is found.
+ mWizard.setSigningInfo(null, null);
+ mPrivateKey = null;
+ mCertificate = null;
+ mKeyDetails = null;
+
+ if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
+ int validity = mWizard.getValidity();
+ StringBuilder sb = new StringBuilder(
+ String.format("<p>Certificate expires in %d years.</p>",
+ validity));
+
+ if (validity < REQUIRED_YEARS) {
+ sb.append(VALIDITY_WARNING);
+ }
+
+ mKeyDetails = sb.toString();
+ } else {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mWizard.getKeystore());
+ keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
+ fis.close();
+ PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ mWizard.getKeyAlias(),
+ new KeyStore.PasswordProtection(
+ mWizard.getKeyPassword().toCharArray()));
+
+ if (entry != null) {
+ mPrivateKey = entry.getPrivateKey();
+ mCertificate = (X509Certificate)entry.getCertificate();
+ } else {
+ setErrorMessage("Unable to find key.");
+
+ setPageComplete(false);
+ }
+ } catch (FileNotFoundException e) {
+ // this was checked at the first previous step and will not happen here, unless
+ // the file was removed during the export wizard execution.
+ onException(e);
+ } catch (KeyStoreException e) {
+ onException(e);
+ } catch (NoSuchAlgorithmException e) {
+ onException(e);
+ } catch (UnrecoverableEntryException e) {
+ onException(e);
+ } catch (CertificateException e) {
+ onException(e);
+ } catch (IOException e) {
+ onException(e);
+ }
+
+ if (mPrivateKey != null && mCertificate != null) {
+ Calendar expirationCalendar = Calendar.getInstance();
+ expirationCalendar.setTime(mCertificate.getNotAfter());
+ Calendar today = Calendar.getInstance();
+
+ if (expirationCalendar.before(today)) {
+ mKeyDetails = String.format(
+ "<p>Certificate expired on %s</p>",
+ mCertificate.getNotAfter().toString());
+
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+
+ setErrorMessage("Certificate is expired.");
+ setPageComplete(false);
+ } else {
+ // valid, key/cert: put it in the wizard so that it can be finished
+ mWizard.setSigningInfo(mPrivateKey, mCertificate);
+
+ StringBuilder sb = new StringBuilder(String.format(
+ "<p>Certificate expires on %s.</p>",
+ mCertificate.getNotAfter().toString()));
+
+ int expirationYear = expirationCalendar.get(Calendar.YEAR);
+ int thisYear = today.get(Calendar.YEAR);
+
+ if (thisYear + REQUIRED_YEARS < expirationYear) {
+ // do nothing
+ } else {
+ if (expirationYear == thisYear) {
+ sb.append("<p>The certificate expires this year.</p>");
+ } else {
+ int count = expirationYear-thisYear;
+ sb.append(String.format(
+ "<p>The Certificate expires in %1$s %2$s.</p>",
+ count, count == 1 ? "year" : "years"));
+ }
+ sb.append(VALIDITY_WARNING);
+ }
+
+ // show certificate fingerprints
+ String sha1 = mWizard.getCertSha1Fingerprint();
+ String md5 = mWizard.getCertMd5Fingerprint();
+
+ sb.append("<p></p>" /*blank line*/);
+ sb.append("<p>Certificate fingerprints:</p>");
+ sb.append(String.format("<li>MD5 : %s</li>", md5));
+ sb.append(String.format("<li>SHA1: %s</li>", sha1));
+ sb.append("<p></p>" /*blank line*/);
+
+ mKeyDetails = sb.toString();
+ }
+ } else {
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+ }
+ }
+ }
+
+ onDestinationChange(true /*forceDetailUpdate*/);
+ }
+
+ /**
+ * Callback for destination field edition
+ * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal
+ * error has happened in the signing.
+ */
+ private void onDestinationChange(boolean forceDetailUpdate) {
+ if (mFatalSigningError == false) {
+ // reset messages for now.
+ setErrorMessage(null);
+ setMessage(null);
+
+ String path = mDestination.getText().trim();
+
+ if (path.length() == 0) {
+ setErrorMessage("Enter destination for the APK file.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ File file = new File(path);
+ if (file.isDirectory()) {
+ setErrorMessage("Destination is a directory.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ File parentFolder = file.getParentFile();
+ if (parentFolder == null || parentFolder.isDirectory() == false) {
+ setErrorMessage("Not a valid directory.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ if (file.isFile()) {
+ mDestinationDetails = "<li>WARNING: destination file already exists</li>";
+ setMessage("Destination file already exists.", WARNING);
+ }
+
+ // no error, set the destination in the wizard.
+ mWizard.setDestination(file);
+ setPageComplete(true);
+
+ updateDetailText();
+ } else if (forceDetailUpdate) {
+ updateDetailText();
+ }
+ }
+
+ /**
+ * Updates the scrollbar to match the content of the {@link FormText} or the new size
+ * of the {@link ScrolledComposite}.
+ */
+ private void updateScrolling() {
+ if (mDetailText != null) {
+ Rectangle r = mScrolledComposite.getClientArea();
+ mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT));
+ mScrolledComposite.layout();
+ }
+ }
+
+ private void updateDetailText() {
+ StringBuilder sb = new StringBuilder("<form>");
+ if (mKeyDetails != null) {
+ sb.append(mKeyDetails);
+ }
+
+ if (mDestinationDetails != null && mFatalSigningError == false) {
+ sb.append(mDestinationDetails);
+ }
+
+ sb.append("</form>");
+
+ mDetailText.setText(sb.toString(), true /* parseTags */,
+ true /* expandURLs */);
+
+ mDetailText.getParent().layout();
+
+ updateScrolling();
+ }
+
+ @Override
+ protected void onException(Throwable t) {
+ super.onException(t);
+
+ mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java
new file mode 100644
index 000000000..aea94ad8d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.List;
+
+/**
+ * Key creation page.
+ */
+final class KeyCreationPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Text mAlias;
+ private Text mKeyPassword;
+ private Text mKeyPassword2;
+ private Text mCnField;
+ private boolean mDisableOnChange = false;
+ private Text mOuField;
+ private Text mOField;
+ private Text mLField;
+ private Text mStField;
+ private Text mCField;
+ private String mDName;
+ private int mValidity = 0;
+ private List<String> mExistingAliases;
+
+
+ protected KeyCreationPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Key Creation");
+ setDescription(""); // TODO?
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(2, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ new Label(composite, SWT.NONE).setText("Alias:");
+ mAlias = new Text(composite, SWT.BORDER);
+ mAlias.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Password:");
+ mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeyPassword.addVerifyListener(sPasswordVerifier);
+
+ new Label(composite, SWT.NONE).setText("Confirm:");
+ mKeyPassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeyPassword2.addVerifyListener(sPasswordVerifier);
+
+ new Label(composite, SWT.NONE).setText("Validity (years):");
+ final Text validityText = new Text(composite, SWT.BORDER);
+ validityText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ validityText.addVerifyListener(new VerifyListener() {
+ @Override
+ public void verifyText(VerifyEvent e) {
+ // check for digit only.
+ for (int i = 0 ; i < e.text.length(); i++) {
+ char letter = e.text.charAt(i);
+ if (letter < '0' || letter > '9') {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ });
+
+ new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(
+ gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+
+ new Label(composite, SWT.NONE).setText("First and Last Name:");
+ mCnField = new Text(composite, SWT.BORDER);
+ mCnField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Organizational Unit:");
+ mOuField = new Text(composite, SWT.BORDER);
+ mOuField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Organization:");
+ mOField = new Text(composite, SWT.BORDER);
+ mOField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("City or Locality:");
+ mLField = new Text(composite, SWT.BORDER);
+ mLField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("State or Province:");
+ mStField = new Text(composite, SWT.BORDER);
+ mStField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Country Code (XX):");
+ mCField = new Text(composite, SWT.BORDER);
+ mCField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mAlias.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyAlias(mAlias.getText().trim());
+ onChange();
+ }
+ });
+ mKeyPassword.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyPassword(mKeyPassword.getText());
+ onChange();
+ }
+ });
+ mKeyPassword2.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onChange();
+ }
+ });
+
+ validityText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ try {
+ mValidity = Integer.parseInt(validityText.getText());
+ } catch (NumberFormatException e2) {
+ // this should only happen if the text field is empty due to the verifyListener.
+ mValidity = 0;
+ }
+ mWizard.setValidity(mValidity);
+ onChange();
+ }
+ });
+
+ ModifyListener dNameListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onDNameChange();
+ }
+ };
+
+ mCnField.addModifyListener(dNameListener);
+ mOuField.addModifyListener(dNameListener);
+ mOField.addModifyListener(dNameListener);
+ mLField.addModifyListener(dNameListener);
+ mStField.addModifyListener(dNameListener);
+ mCField.addModifyListener(dNameListener);
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) {
+ // reset the keystore/alias from the content of the project
+ IProject project = mWizard.getProject();
+
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ String alias = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_ALIAS);
+ if (alias != null) {
+ mAlias.setText(alias);
+ }
+
+ // get the existing list of keys if applicable
+ if (mWizard.getKeyCreationMode()) {
+ mExistingAliases = mWizard.getExistingAliases();
+ } else {
+ mExistingAliases = null;
+ }
+
+ // reset the passwords
+ mKeyPassword.setText(""); //$NON-NLS-1$
+ mKeyPassword2.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ }
+ }
+
+ @Override
+ public IWizardPage getPreviousPage() {
+ if (mWizard.getKeyCreationMode()) { // this means we create a key from an existing store
+ return mWizard.getKeySelectionPage();
+ }
+
+ return mWizard.getKeystoreSelectionPage();
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ return mWizard.getKeyCheckPage();
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mAlias.getText().trim().length() == 0) {
+ setErrorMessage("Enter key alias.");
+ setPageComplete(false);
+ return;
+ } else if (mExistingAliases != null) {
+ // we cannot use indexOf, because we need to do a case-insensitive check
+ String keyAlias = mAlias.getText().trim();
+ for (String alias : mExistingAliases) {
+ if (alias.equalsIgnoreCase(keyAlias)) {
+ setErrorMessage("Key alias already exists in keystore.");
+ setPageComplete(false);
+ return;
+ }
+ }
+ }
+
+ String value = mKeyPassword.getText();
+ if (value.length() == 0) {
+ setErrorMessage("Enter key password.");
+ setPageComplete(false);
+ return;
+ } else if (value.length() < 6) {
+ setErrorMessage("Key password is too short - must be at least 6 characters.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (value.equals(mKeyPassword2.getText()) == false) {
+ setErrorMessage("Key passwords don't match.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mValidity == 0) {
+ setErrorMessage("Key certificate validity is required.");
+ setPageComplete(false);
+ return;
+ } else if (mValidity < 25) {
+ setMessage("A 25 year certificate validity is recommended.", WARNING);
+ } else if (mValidity > 1000) {
+ setErrorMessage("Key certificate validity must be between 1 and 1000 years.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mDName == null || mDName.length() == 0) {
+ setErrorMessage("At least one Certificate issuer field is required to be non-empty.");
+ setPageComplete(false);
+ return;
+ }
+
+ setPageComplete(true);
+ }
+
+ /**
+ * Handles changes in the DName fields.
+ */
+ private void onDNameChange() {
+ StringBuilder sb = new StringBuilder();
+
+ buildDName("CN", mCnField, sb);
+ buildDName("OU", mOuField, sb);
+ buildDName("O", mOField, sb);
+ buildDName("L", mLField, sb);
+ buildDName("ST", mStField, sb);
+ buildDName("C", mCField, sb);
+
+ mDName = sb.toString();
+ mWizard.setDName(mDName);
+
+ onChange();
+ }
+
+ /**
+ * Builds the distinguished name string with the provided {@link StringBuilder}.
+ * @param prefix the prefix of the entry.
+ * @param textField The {@link Text} field containing the entry value.
+ * @param sb the string builder containing the dname.
+ */
+ private void buildDName(String prefix, Text textField, StringBuilder sb) {
+ if (textField != null) {
+ String value = textField.getText().trim();
+ if (value.length() > 0) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+
+ sb.append(prefix);
+ sb.append('=');
+ sb.append(value);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java
new file mode 100644
index 000000000..604a208e6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Key Selection Page. This is used when an existing keystore is used.
+ */
+final class KeySelectionPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Label mKeyAliasesLabel;
+ private Combo mKeyAliases;
+ private Label mKeyPasswordLabel;
+ private Text mKeyPassword;
+ private boolean mDisableOnChange = false;
+ private Button mUseExistingKey;
+ private Button mCreateKey;
+
+ protected KeySelectionPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Key alias selection");
+ setDescription(""); // TODO
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ mUseExistingKey = new Button(composite, SWT.RADIO);
+ mUseExistingKey.setText("Use existing key");
+ mUseExistingKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ mUseExistingKey.setSelection(true);
+
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = 0;
+ gd.widthHint = 50;
+ mKeyAliasesLabel = new Label(composite, SWT.NONE);
+ mKeyAliasesLabel.setText("Alias:");
+ mKeyAliases = new Combo(composite, SWT.READ_ONLY);
+ mKeyAliases.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = 0;
+ gd.widthHint = 50;
+ mKeyPasswordLabel = new Label(composite, SWT.NONE);
+ mKeyPasswordLabel.setText("Password:");
+ mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mCreateKey = new Button(composite, SWT.RADIO);
+ mCreateKey.setText("Create new key");
+ mCreateKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mUseExistingKey.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mWizard.setKeyCreationMode(!mUseExistingKey.getSelection());
+ enableWidgets();
+ onChange();
+ }
+ });
+
+ mKeyAliases.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mWizard.setKeyAlias(mKeyAliases.getItem(mKeyAliases.getSelectionIndex()));
+ onChange();
+ }
+ });
+
+ mKeyPassword.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyPassword(mKeyPassword.getText());
+ onChange();
+ }
+ });
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) {
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ // reset the alias from the content of the project
+ try {
+ // reset to using a key
+ mWizard.setKeyCreationMode(false);
+ mUseExistingKey.setSelection(true);
+ mCreateKey.setSelection(false);
+ enableWidgets();
+
+ // remove the content of the alias combo always and first, in case the
+ // keystore password is wrong
+ mKeyAliases.removeAll();
+
+ // get the alias list (also used as a keystore password test)
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mWizard.getKeystore());
+ keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
+ fis.close();
+
+ Enumeration<String> aliases = keyStore.aliases();
+
+ // get the alias from the project previous export, and look for a match as
+ // we add the aliases to the combo.
+ IProject project = mWizard.getProject();
+
+ String keyAlias = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_ALIAS);
+
+ ArrayList<String> aliasList = new ArrayList<String>();
+
+ int selection = -1;
+ int count = 0;
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ mKeyAliases.add(alias);
+ aliasList.add(alias);
+ if (selection == -1 && alias.equalsIgnoreCase(keyAlias)) {
+ selection = count;
+ }
+ count++;
+ }
+
+ mWizard.setExistingAliases(aliasList);
+
+ if (selection != -1) {
+ mKeyAliases.select(selection);
+
+ // since a match was found and is selected, we need to give it to
+ // the wizard as well
+ mWizard.setKeyAlias(keyAlias);
+ } else {
+ mKeyAliases.clearSelection();
+ }
+
+ // reset the password
+ mKeyPassword.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ } catch (KeyStoreException e) {
+ onException(e);
+ } catch (FileNotFoundException e) {
+ onException(e);
+ } catch (NoSuchAlgorithmException e) {
+ onException(e);
+ } catch (CertificateException e) {
+ onException(e);
+ } catch (IOException e) {
+ onException(e);
+ } finally {
+ // in case we exit with an exception, we need to reset this
+ mDisableOnChange = false;
+ }
+ }
+ }
+
+ @Override
+ public IWizardPage getPreviousPage() {
+ return mWizard.getKeystoreSelectionPage();
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ if (mWizard.getKeyCreationMode()) {
+ return mWizard.getKeyCreationPage();
+ }
+
+ return mWizard.getKeyCheckPage();
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mWizard.getKeyCreationMode() == false) {
+ if (mKeyAliases.getSelectionIndex() == -1) {
+ setErrorMessage("Select a key alias.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mKeyPassword.getText().trim().length() == 0) {
+ setErrorMessage("Enter key password.");
+ setPageComplete(false);
+ return;
+ }
+ }
+
+ setPageComplete(true);
+ }
+
+ private void enableWidgets() {
+ boolean useKey = !mWizard.getKeyCreationMode();
+ mKeyAliasesLabel.setEnabled(useKey);
+ mKeyAliases.setEnabled(useKey);
+ mKeyPassword.setEnabled(useKey);
+ mKeyPasswordLabel.setEnabled(useKey);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java
new file mode 100644
index 000000000..eabee15a2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+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.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/**
+ * Keystore selection page. This page allows to choose to create a new keystore or use an
+ * existing one.
+ */
+final class KeystoreSelectionPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Button mUseExistingKeystore;
+ private Button mCreateKeystore;
+ private Text mKeystore;
+ private Text mKeystorePassword;
+ private Label mConfirmLabel;
+ private Text mKeystorePassword2;
+ private boolean mDisableOnChange = false;
+
+ protected KeystoreSelectionPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Keystore selection");
+ setDescription(""); //TODO
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ mUseExistingKeystore = new Button(composite, SWT.RADIO);
+ mUseExistingKeystore.setText("Use existing keystore");
+ mUseExistingKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ mUseExistingKeystore.setSelection(true);
+
+ mCreateKeystore = new Button(composite, SWT.RADIO);
+ mCreateKeystore.setText("Create new keystore");
+ mCreateKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+
+ new Label(composite, SWT.NONE).setText("Location:");
+ mKeystore = new Text(composite, SWT.BORDER);
+ mKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ final Button browseButton = new Button(composite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog;
+ if (mUseExistingKeystore.getSelection()) {
+ fileDialog = new FileDialog(browseButton.getShell(),SWT.OPEN);
+ fileDialog.setText("Load Keystore");
+ } else {
+ fileDialog = new FileDialog(browseButton.getShell(),SWT.SAVE);
+ fileDialog.setText("Select Keystore Name");
+ }
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ mKeystore.setText(fileName);
+ }
+ }
+ });
+
+ new Label(composite, SWT.NONE).setText("Password:");
+ mKeystorePassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeystorePassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeystorePassword.addVerifyListener(sPasswordVerifier);
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+
+ mConfirmLabel = new Label(composite, SWT.NONE);
+ mConfirmLabel.setText("Confirm:");
+ mKeystorePassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeystorePassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeystorePassword2.addVerifyListener(sPasswordVerifier);
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+ mKeystorePassword2.setEnabled(false);
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mUseExistingKeystore.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean createStore = !mUseExistingKeystore.getSelection();
+ mKeystorePassword2.setEnabled(createStore);
+ mConfirmLabel.setEnabled(createStore);
+ mWizard.setKeystoreCreationMode(createStore);
+ onChange();
+ }
+ });
+
+ mKeystore.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeystore(mKeystore.getText().trim());
+ onChange();
+ }
+ });
+
+ mKeystorePassword.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeystorePassword(mKeystorePassword.getText());
+ onChange();
+ }
+ });
+
+ mKeystorePassword2.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onChange();
+ }
+ });
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ if (mUseExistingKeystore.getSelection()) {
+ return mWizard.getKeySelectionPage();
+ }
+
+ return mWizard.getKeyCreationPage();
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & DATA_PROJECT) != 0) {
+ // reset the keystore/alias from the content of the project
+ IProject project = mWizard.getProject();
+
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ String keystore = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_KEYSTORE);
+ if (keystore != null) {
+ mKeystore.setText(keystore);
+ }
+
+ // reset the passwords
+ mKeystorePassword.setText(""); //$NON-NLS-1$
+ mKeystorePassword2.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ }
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ boolean createStore = !mUseExistingKeystore.getSelection();
+
+ // checks the keystore path is non null.
+ String keystore = mKeystore.getText().trim();
+ if (keystore.length() == 0) {
+ setErrorMessage("Enter path to keystore.");
+ setPageComplete(false);
+ return;
+ } else {
+ File f = new File(keystore);
+ if (f.exists() == false) {
+ if (createStore == false) {
+ setErrorMessage("Keystore does not exist.");
+ setPageComplete(false);
+ return;
+ }
+ } else if (f.isDirectory()) {
+ setErrorMessage("Keystore path is a directory.");
+ setPageComplete(false);
+ return;
+ } else if (f.isFile()) {
+ if (createStore) {
+ setErrorMessage("File already exists.");
+ setPageComplete(false);
+ return;
+ }
+ }
+ }
+
+ String value = mKeystorePassword.getText();
+ if (value.length() == 0) {
+ setErrorMessage("Enter keystore password.");
+ setPageComplete(false);
+ return;
+ } else if (createStore && value.length() < 6) {
+ setErrorMessage("Keystore password is too short - must be at least 6 characters.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (createStore) {
+ if (mKeystorePassword2.getText().length() == 0) {
+ setErrorMessage("Confirm keystore password.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mKeystorePassword.getText().equals(mKeystorePassword2.getText()) == false) {
+ setErrorMessage("Keystore passwords do not match.");
+ setPageComplete(false);
+ return;
+ }
+ }
+
+ setPageComplete(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java
new file mode 100644
index 000000000..b8a7043da
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.NonLibraryProjectOnlyFilter;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+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.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * First Export Wizard Page. Display warning/errors.
+ */
+final class ProjectCheckPage extends ExportWizardPage {
+ private final static String IMG_ERROR = "error.png"; //$NON-NLS-1$
+ private final static String IMG_WARNING = "warning.png"; //$NON-NLS-1$
+
+ private final ExportWizard mWizard;
+ private Image mError;
+ private Image mWarning;
+ private boolean mHasMessage = false;
+ private Composite mTopComposite;
+ private Composite mErrorComposite;
+ private Text mProjectText;
+ private ProjectChooserHelper mProjectChooserHelper;
+ private boolean mFirstOnShow = true;
+
+ protected ProjectCheckPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Project Checks");
+ setDescription("Performs a set of checks to make sure the application can be exported.");
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(),
+ new NonLibraryProjectOnlyFilter());
+
+ GridLayout gl = null;
+ GridData gd = null;
+
+ mTopComposite = new Composite(parent, SWT.NONE);
+ mTopComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mTopComposite.setLayout(new GridLayout(1, false));
+
+ // composite for the project selection.
+ Composite projectComposite = new Composite(mTopComposite, SWT.NONE);
+ projectComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ projectComposite.setLayout(gl = new GridLayout(3, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Label label = new Label(projectComposite, SWT.NONE);
+ label.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ label.setText("Select the project to export:");
+
+ new Label(projectComposite, SWT.NONE).setText("Project:");
+ mProjectText = new Text(projectComposite, SWT.BORDER);
+ mProjectText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mProjectText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ handleProjectNameChange();
+ }
+ });
+
+ Button browseButton = new Button(projectComposite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject(
+ mProjectText.getText().trim(),
+ "Please select a project to export");
+
+ if (javaProject != null) {
+ IProject project = javaProject.getProject();
+
+ // set the new name in the text field. The modify listener will take
+ // care of updating the status and the ExportWizard object.
+ mProjectText.setText(project.getName());
+ }
+ }
+ });
+
+ setControl(mTopComposite);
+ }
+
+ @Override
+ void onShow() {
+ if (mFirstOnShow) {
+ // get the project and init the ui
+ IProject project = mWizard.getProject();
+ if (project != null) {
+ mProjectText.setText(project.getName());
+ }
+
+ mFirstOnShow = false;
+ }
+ }
+
+ private void buildErrorUi(IProject project) {
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setPageComplete(true);
+ mHasMessage = false;
+
+ // composite parent for the warning/error
+ GridLayout gl = null;
+ mErrorComposite = new Composite(mTopComposite, SWT.NONE);
+ mErrorComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing *= 3; // more spacing than normal.
+ mErrorComposite.setLayout(gl);
+
+ if (project == null) {
+ setErrorMessage("Select project to export.");
+ mHasMessage = true;
+ } else {
+ try {
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ addError(mErrorComposite, "Project is not an Android project.");
+ } else {
+ // check for errors
+ if (ProjectHelper.hasError(project, true)) {
+ addError(mErrorComposite, "Project has compilation error(s)");
+ }
+
+ // check the project output
+ IFolder outputIFolder = BaseProjectHelper.getJavaOutputFolder(project);
+ if (outputIFolder == null) {
+ addError(mErrorComposite,
+ "Unable to get the output folder of the project!");
+ }
+
+ // project is an android project, we check the debuggable attribute.
+ ManifestData manifestData = AndroidManifestHelper.parseForData(project);
+ Boolean debuggable = null;
+ if (manifestData != null) {
+ debuggable = manifestData.getDebuggable();
+ }
+
+ if (debuggable != null && debuggable == Boolean.TRUE) {
+ addWarning(mErrorComposite,
+ "The manifest 'debuggable' attribute is set to true.\n" +
+ "You should set it to false for applications that you release to the public.\n\n" +
+ "Applications with debuggable=true are compiled in debug mode always.");
+ }
+
+ // check for mapview stuff
+ }
+ } catch (CoreException e) {
+ // unable to access nature
+ addError(mErrorComposite, "Unable to get project nature");
+ }
+ }
+
+ if (mHasMessage == false) {
+ Label label = new Label(mErrorComposite, SWT.NONE);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ label.setLayoutData(gd);
+ label.setText("No errors found. Click Next.");
+ }
+
+ mTopComposite.layout();
+ }
+
+ /**
+ * Adds an error label to a {@link Composite} object.
+ * @param parent the Composite parent.
+ * @param message the error message.
+ */
+ private void addError(Composite parent, String message) {
+ if (mError == null) {
+ mError = IconFactory.getInstance().getIcon(IMG_ERROR);
+ }
+
+ new Label(parent, SWT.NONE).setImage(mError);
+ Label label = new Label(parent, SWT.NONE);
+ label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ label.setText(message);
+
+ setErrorMessage("Application cannot be exported due to the error(s) below.");
+ setPageComplete(false);
+ mHasMessage = true;
+ }
+
+ /**
+ * Adds a warning label to a {@link Composite} object.
+ * @param parent the Composite parent.
+ * @param message the warning message.
+ */
+ private void addWarning(Composite parent, String message) {
+ if (mWarning == null) {
+ mWarning = IconFactory.getInstance().getIcon(IMG_WARNING);
+ }
+
+ new Label(parent, SWT.NONE).setImage(mWarning);
+ Label label = new Label(parent, SWT.NONE);
+ label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ label.setText(message);
+
+ mHasMessage = true;
+ }
+
+ /**
+ * Checks the parameters for correctness, and update the error message and buttons.
+ */
+ private void handleProjectNameChange() {
+ setPageComplete(false);
+
+ if (mErrorComposite != null) {
+ mErrorComposite.dispose();
+ mErrorComposite = null;
+ }
+
+ // update the wizard with the new project
+ mWizard.setProject(null);
+
+ //test the project name first!
+ String text = mProjectText.getText().trim();
+ if (text.length() == 0) {
+ setErrorMessage("Select project to export.");
+ } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) {
+ setErrorMessage("Project name contains unsupported characters!");
+ } else {
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
+ IProject found = null;
+ for (IJavaProject javaProject : projects) {
+ if (javaProject.getProject().getName().equals(text)) {
+ found = javaProject.getProject();
+ break;
+ }
+
+ }
+
+ if (found != null) {
+ setErrorMessage(null);
+
+ // update the wizard with the new project
+ mWizard.setProject(found);
+
+ // now rebuild the error ui.
+ buildErrorUi(found);
+ } else {
+ setErrorMessage(String.format("There is no android project named '%1$s'",
+ text));
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java
new file mode 100644
index 000000000..d3df0584f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import static com.android.SdkConstants.GRADLE_LATEST_VERSION;
+import static com.android.SdkConstants.GRADLE_PLUGIN_LATEST_VERSION;
+import static com.android.SdkConstants.GRADLE_PLUGIN_NAME;
+import static com.android.tools.lint.checks.GradleDetector.APP_PLUGIN_ID;
+import static com.android.tools.lint.checks.GradleDetector.LIB_PLUGIN_ID;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.io.IAbstractFile;
+import com.android.sdklib.io.FileOp;
+import com.android.xml.AndroidManifest;
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.io.Closeables;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Creates build.gradle and settings.gradle files for a set of projects.
+ * <p>
+ * Based on {@link org.eclipse.ant.internal.ui.datatransfer.BuildFileCreator}
+ */
+public class BuildFileCreator {
+ static final String BUILD_FILE = "build.gradle"; //$NON-NLS-1$
+ static final String SETTINGS_FILE = "settings.gradle"; //$NON-NLS-1$
+ private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
+ private static final String GRADLE_WRAPPER_LOCATION =
+ "tools/templates/gradle/wrapper"; //$NON-NLS-1$
+ static final String PLUGIN_CLASSPATH =
+ "classpath '" + GRADLE_PLUGIN_NAME + GRADLE_PLUGIN_LATEST_VERSION + "'"; //$NON-NLS-1$
+ static final String MAVEN_REPOSITORY = "jcenter()"; //$NON-NLS-1$
+
+ private static final String[] GRADLE_WRAPPER_FILES = new String[] {
+ "gradlew", //$NON-NLS-1$
+ "gradlew.bat", //$NON-NLS-1$
+ "gradle/wrapper/gradle-wrapper.jar", //$NON-NLS-1$
+ "gradle/wrapper/gradle-wrapper.properties" //$NON-NLS-1$
+ };
+
+ private static final Comparator<IFile> FILE_COMPARATOR = new Comparator<IFile>() {
+ @Override
+ public int compare(IFile o1, IFile o2) {
+ return o1.toString().compareTo(o2.toString());
+ }
+ };
+
+ private final GradleModule mModule;
+ private final StringBuilder mBuildFile = new StringBuilder();
+
+ /**
+ * Create buildfile for the projects.
+ *
+ * @param shell parent instance for dialogs
+ * @return project names for which buildfiles were created
+ * @throws InterruptedException thrown when user cancels task
+ */
+ public static void createBuildFiles(
+ @NonNull ProjectSetupBuilder builder,
+ @NonNull Shell shell,
+ @NonNull IProgressMonitor pm) {
+
+ File gradleLocation = new File(Sdk.getCurrent().getSdkOsLocation(), GRADLE_WRAPPER_LOCATION);
+ SubMonitor localmonitor = null;
+
+ try {
+ // See if we have a Gradle wrapper in the SDK templates directory. If so, we can copy
+ // it over.
+ boolean hasGradleWrapper = true;
+ for (File wrapperFile : getGradleWrapperFiles(gradleLocation)) {
+ if (!wrapperFile.exists()) {
+ hasGradleWrapper = false;
+ }
+ }
+
+ Collection<GradleModule> modules = builder.getModules();
+ boolean multiModules = modules.size() > 1;
+
+ // determine files to create/change
+ List<IFile> files = new ArrayList<IFile>();
+
+ // add the build.gradle file for all modules.
+ for (GradleModule module : modules) {
+ // build.gradle file
+ IFile file = module.getProject().getFile(BuildFileCreator.BUILD_FILE);
+ files.add(file);
+ }
+
+ // get the commonRoot for all modules. If only one module, this returns the path
+ // of the project.
+ IPath commonRoot = builder.getCommonRoot();
+
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IPath workspaceLocation = workspaceRoot.getLocation();
+
+ IPath relativePath = commonRoot.makeRelativeTo(workspaceLocation);
+ // if makeRelativePath to returns the same path, then commonRoot is not in the
+ // workspace.
+ boolean rootInWorkspace = !relativePath.equals(commonRoot);
+ // we only care if the root is a workspace project. if it's the workspace folder itself,
+ // then the files won't be handled by the workspace.
+ rootInWorkspace = rootInWorkspace && relativePath.segmentCount() > 0;
+
+ File settingsFile = new File(commonRoot.toFile(), SETTINGS_FILE);
+
+ // more than one modules -> generate settings.gradle
+ if (multiModules && rootInWorkspace) {
+
+ // Locate the settings.gradle file and add it to the changed files list
+ IPath settingsGradle = Path.fromOSString(settingsFile.getAbsolutePath());
+
+ // different path, means commonRoot is inside the workspace, which means we have
+ // to add settings.gradle and wrapper files to the list of files to add.
+ IFile iFile = workspaceRoot.getFile(settingsGradle);
+ if (iFile != null) {
+ files.add(iFile);
+ }
+ }
+
+ // Gradle wrapper files
+ if (hasGradleWrapper && rootInWorkspace) {
+ // See if there already wrapper files there and only mark nonexistent ones for
+ // creation.
+ for (File wrapperFile : getGradleWrapperFiles(commonRoot.toFile())) {
+ if (!wrapperFile.exists()) {
+ IPath path = Path.fromOSString(wrapperFile.getAbsolutePath());
+ IFile file = workspaceRoot.getFile(path);
+ files.add(file);
+ }
+ }
+ }
+
+ ExportStatus status = new ExportStatus();
+ builder.setStatus(status);
+
+ // Trigger checkout of changed files
+ Set<IFile> confirmedFiles = validateEdit(files, status, shell);
+
+ if (status.hasError()) {
+ return;
+ }
+
+ // Now iterate over all the modules and generate the build files.
+ localmonitor = SubMonitor.convert(pm, ExportMessages.PageTitle,
+ confirmedFiles.size());
+ List<String> projectSettingsPath = Lists.newArrayList();
+ for (GradleModule currentModule : modules) {
+ IProject moduleProject = currentModule.getProject();
+
+ IFile file = moduleProject.getFile(BuildFileCreator.BUILD_FILE);
+ if (!confirmedFiles.contains(file)) {
+ continue;
+ }
+
+ localmonitor.setTaskName(NLS.bind(ExportMessages.FileStatusMessage,
+ moduleProject.getName()));
+
+ ProjectState projectState = Sdk.getProjectState(moduleProject);
+ BuildFileCreator instance = new BuildFileCreator(currentModule, shell);
+ if (projectState != null) {
+ // This is an Android project
+ if (!multiModules) {
+ instance.appendBuildScript();
+ }
+ instance.appendHeader(projectState.isLibrary());
+ instance.appendDependencies();
+ instance.startAndroidTask(projectState);
+ //instance.appendDefaultConfig();
+ instance.createAndroidSourceSets();
+ instance.finishAndroidTask();
+ } else {
+ // This is a plain Java project
+ instance.appendJavaHeader();
+ instance.createJavaSourceSets();
+ }
+
+ try {
+ // Write the build file
+ String buildfile = instance.mBuildFile.toString();
+ InputStream is =
+ new ByteArrayInputStream(buildfile.getBytes("UTF-8")); //$NON-NLS-1$
+ if (file.exists()) {
+ file.setContents(is, true, true, null);
+ } else {
+ file.create(is, true, null);
+ }
+ } catch (Exception e) {
+ status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE,
+ file.getLocation().toFile());
+ status.setErrorMessage(e.getMessage());
+ return;
+ }
+
+ if (localmonitor.isCanceled()) {
+ return;
+ }
+ localmonitor.worked(1);
+
+ // get the project path to add it to the settings.gradle.
+ projectSettingsPath.add(currentModule.getPath());
+ }
+
+ // write the settings file.
+ if (multiModules) {
+ try {
+ writeGradleSettingsFile(settingsFile, projectSettingsPath);
+ } catch (IOException e) {
+ status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, settingsFile);
+ status.setErrorMessage(e.getMessage());
+ return;
+ }
+ File mainBuildFile = new File(commonRoot.toFile(), BUILD_FILE);
+ try {
+ writeRootBuildGradle(mainBuildFile);
+ } catch (IOException e) {
+ status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, mainBuildFile);
+ status.setErrorMessage(e.getMessage());
+ return;
+ }
+ }
+
+ // finally write the wrapper
+ // TODO check we can based on where it is
+ if (hasGradleWrapper) {
+ copyGradleWrapper(gradleLocation, commonRoot.toFile(), status);
+ if (status.hasError()) {
+ return;
+ }
+ }
+
+ } finally {
+ if (localmonitor != null && !localmonitor.isCanceled()) {
+ localmonitor.done();
+ }
+ if (pm != null) {
+ pm.done();
+ }
+ }
+ }
+
+ /**
+ * @param GradleModule create buildfile for this project
+ * @param shell parent instance for dialogs
+ */
+ private BuildFileCreator(GradleModule module, Shell shell) {
+ mModule = module;
+ }
+
+ /**
+ * Return the files that comprise the Gradle wrapper as a collection of {@link File} instances.
+ * @param root
+ * @return
+ */
+ private static List<File> getGradleWrapperFiles(File root) {
+ List<File> files = new ArrayList<File>(GRADLE_WRAPPER_FILES.length);
+ for (String file : GRADLE_WRAPPER_FILES) {
+ files.add(new File(root, file));
+ }
+ return files;
+ }
+
+ /**
+ * Copy the Gradle wrapper files from one directory to another.
+ */
+ private static void copyGradleWrapper(File from, File to, ExportStatus status) {
+ for (String file : GRADLE_WRAPPER_FILES) {
+ File dest = new File(to, file);
+ try {
+ File src = new File(from, file);
+ dest.getParentFile().mkdirs();
+ new FileOp().copyFile(src, dest);
+
+ if (src.getName().equals(GRADLE_PROPERTIES)) {
+ updateGradleDistributionUrl(GRADLE_LATEST_VERSION, dest);
+ }
+ dest.setExecutable(src.canExecute());
+ status.addFileStatus(ExportStatus.FileStatus.OK, dest);
+ } catch (IOException e) {
+ status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, dest);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Outputs boilerplate buildscript information common to all Gradle build files.
+ */
+ private void appendBuildScript() {
+ appendBuildScript(mBuildFile);
+ }
+
+ /**
+ * Outputs boilerplate header information common to all Gradle build files.
+ */
+ private static void appendBuildScript(StringBuilder builder) {
+ builder.append("buildscript {\n"); //$NON-NLS-1$
+ builder.append(" repositories {\n"); //$NON-NLS-1$
+ builder.append(" " + MAVEN_REPOSITORY + "\n"); //$NON-NLS-1$
+ builder.append(" }\n"); //$NON-NLS-1$
+ builder.append(" dependencies {\n"); //$NON-NLS-1$
+ builder.append(" " + PLUGIN_CLASSPATH + "\n"); //$NON-NLS-1$
+ builder.append(" }\n"); //$NON-NLS-1$
+ builder.append("}\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs boilerplate header information common to all Gradle build files.
+ */
+ private void appendHeader(boolean isLibrary) {
+ if (isLibrary) {
+ mBuildFile.append("apply plugin: '").append(LIB_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ } else {
+ mBuildFile.append("apply plugin: '").append(APP_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ mBuildFile.append("\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs a block which sets up library and project dependencies.
+ */
+ private void appendDependencies() {
+ mBuildFile.append("dependencies {\n"); //$NON-NLS-1$
+
+ // first the local jars.
+ // TODO: Fix
+ mBuildFile.append(" compile fileTree(dir: 'libs', include: '*.jar')\n"); //$NON-NLS-1$
+
+ for (GradleModule dep : mModule.getDependencies()) {
+ mBuildFile.append(" compile project('" + dep.getPath() + "')\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ mBuildFile.append("}\n"); //$NON-NLS-1$
+ mBuildFile.append("\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs the beginning of an Android task in the build file.
+ */
+ private void startAndroidTask(ProjectState projectState) {
+ int buildApi = projectState.getTarget().getVersion().getApiLevel();
+ String toolsVersion = projectState.getTarget().getBuildToolInfo().getRevision().toString();
+ mBuildFile.append("android {\n"); //$NON-NLS-1$
+ mBuildFile.append(" compileSdkVersion " + buildApi + "\n"); //$NON-NLS-1$
+ mBuildFile.append(" buildToolsVersion \"" + toolsVersion + "\"\n"); //$NON-NLS-1$
+ mBuildFile.append("\n"); //$NON-NLS-1$
+
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(projectState.getProject());
+ // otherwise we check source compatibility
+ String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
+ if (JavaCore.VERSION_1_7.equals(source)) {
+ mBuildFile.append(
+ " compileOptions {\n" + //$NON-NLS-1$
+ " sourceCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$
+ " targetCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$
+ " }\n" + //$NON-NLS-1$
+ "\n"); //$NON-NLS-1$
+ }
+ } catch (CoreException e) {
+ // Ignore compliance level, go with default
+ }
+ }
+
+ /**
+ * Outputs a sourceSets block to the Android task that locates all of the various source
+ * subdirectories in the project.
+ */
+ private void createAndroidSourceSets() {
+ IFolderWrapper projectFolder = new IFolderWrapper(mModule.getProject());
+ IAbstractFile mManifestFile = AndroidManifest.getManifest(projectFolder);
+ if (mManifestFile == null) {
+ return;
+ }
+ List<String> srcDirs = new ArrayList<String>();
+ for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) {
+ if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE ||
+ SdkConstants.FD_GEN_SOURCES.equals(entry.getPath().lastSegment())) {
+ continue;
+ }
+ IPath path = entry.getPath().removeFirstSegments(1);
+ srcDirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$
+ }
+
+ String srcPaths = Joiner.on(",").join(srcDirs);
+
+ mBuildFile.append(" sourceSets {\n"); //$NON-NLS-1$
+ mBuildFile.append(" main {\n"); //$NON-NLS-1$
+ mBuildFile.append(" manifest.srcFile '" + SdkConstants.FN_ANDROID_MANIFEST_XML + "'\n"); //$NON-NLS-1$
+ mBuildFile.append(" java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" aidl.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" renderscript.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" res.srcDirs = ['res']\n"); //$NON-NLS-1$
+ mBuildFile.append(" assets.srcDirs = ['assets']\n"); //$NON-NLS-1$
+ mBuildFile.append(" }\n"); //$NON-NLS-1$
+ mBuildFile.append("\n"); //$NON-NLS-1$
+ mBuildFile.append(" // Move the tests to tests/java, tests/res, etc...\n"); //$NON-NLS-1$
+ mBuildFile.append(" instrumentTest.setRoot('tests')\n"); //$NON-NLS-1$
+ if (srcDirs.contains("'src'")) {
+ mBuildFile.append("\n"); //$NON-NLS-1$
+ mBuildFile.append(" // Move the build types to build-types/<type>\n"); //$NON-NLS-1$
+ mBuildFile.append(" // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...\n"); //$NON-NLS-1$
+ mBuildFile.append(" // This moves them out of them default location under src/<type>/... which would\n"); //$NON-NLS-1$
+ mBuildFile.append(" // conflict with src/ being used by the main source set.\n"); //$NON-NLS-1$
+ mBuildFile.append(" // Adding new build types or product flavors should be accompanied\n"); //$NON-NLS-1$
+ mBuildFile.append(" // by a similar customization.\n"); //$NON-NLS-1$
+ mBuildFile.append(" debug.setRoot('build-types/debug')\n"); //$NON-NLS-1$
+ mBuildFile.append(" release.setRoot('build-types/release')\n"); //$NON-NLS-1$
+ }
+ mBuildFile.append(" }\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs the completion of the Android task in the build file.
+ */
+ private void finishAndroidTask() {
+ mBuildFile.append("}\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs a boilerplate header for non-Android projects
+ */
+ private void appendJavaHeader() {
+ mBuildFile.append("apply plugin: 'java'\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Outputs a sourceSets block for non-Android projects to locate the source directories.
+ */
+ private void createJavaSourceSets() {
+ List<String> dirs = new ArrayList<String>();
+ for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) {
+ if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) {
+ continue;
+ }
+ IPath path = entry.getPath().removeFirstSegments(1);
+ dirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$
+ }
+
+ String srcPaths = Joiner.on(",").join(dirs);
+
+ mBuildFile.append("sourceSets {\n"); //$NON-NLS-1$
+ mBuildFile.append(" main.java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" main.resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$
+ mBuildFile.append(" test.java.srcDirs = ['tests/java']\n"); //$NON-NLS-1$
+ mBuildFile.append(" test.resources.srcDirs = ['tests/resources']\n"); //$NON-NLS-1$
+ mBuildFile.append("}\n"); //$NON-NLS-1$
+ }
+
+ /**
+ * Merges the new subproject dependencies into the settings.gradle file if it already exists,
+ * and creates one if it does not.
+ * @throws IOException
+ */
+ private static void writeGradleSettingsFile(File settingsFile, List<String> projectPaths)
+ throws IOException {
+ StringBuilder contents = new StringBuilder();
+ for (String path : projectPaths) {
+ contents.append("include '").append(path).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ Files.write(contents.toString(), settingsFile, Charsets.UTF_8);
+ }
+
+ private static void writeRootBuildGradle(File buildFile) throws IOException {
+ StringBuilder sb = new StringBuilder(
+ "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n");
+
+ appendBuildScript(sb);
+
+ Files.write(sb.toString(), buildFile, Charsets.UTF_8);
+ }
+
+ /**
+ * Request write access to given files. Depending on the version control
+ * plug-in opens a confirm checkout dialog.
+ *
+ * @param shell
+ * parent instance for dialogs
+ * @return <code>IFile</code> objects for which user confirmed checkout
+ * @throws CoreException
+ * thrown if project is under version control, but not connected
+ */
+ static Set<IFile> validateEdit(
+ @NonNull List<IFile> files,
+ @NonNull ExportStatus exportStatus,
+ @NonNull Shell shell) {
+ Set<IFile> confirmedFiles = new TreeSet<IFile>(FILE_COMPARATOR);
+ if (files.size() == 0) {
+ return confirmedFiles;
+ }
+ IStatus status = (files.get(0)).getWorkspace().validateEdit(
+ files.toArray(new IFile[files.size()]), shell);
+ if (status.isMultiStatus() && status.getChildren().length > 0) {
+ for (int i = 0; i < status.getChildren().length; i++) {
+ IStatus statusChild = status.getChildren()[i];
+ if (statusChild.isOK()) {
+ confirmedFiles.add(files.get(i));
+ } else {
+ exportStatus.addFileStatus(
+ ExportStatus.FileStatus.VCS_FAILURE,
+ files.get(i).getLocation().toFile());
+ }
+ }
+ } else if (status.isOK()) {
+ confirmedFiles.addAll(files);
+ }
+ if (status.getSeverity() == IStatus.ERROR) {
+ // not possible to checkout files: not connected to version
+ // control plugin or hijacked files and made read-only, so
+ // collect error messages provided by validator and re-throw
+ StringBuffer message = new StringBuffer(status.getPlugin() + ": " //$NON-NLS-1$
+ + status.getMessage() + NEWLINE);
+ if (status.isMultiStatus()) {
+ for (int i = 0; i < status.getChildren().length; i++) {
+ IStatus statusChild = status.getChildren()[i];
+ message.append(statusChild.getMessage() + NEWLINE);
+ }
+ }
+ String s = message.toString();
+ exportStatus.setErrorMessage(s);
+ }
+
+ return confirmedFiles;
+ }
+
+ // -------------------------------------------------------------------------------
+ // Fix gradle wrapper version. This code is from GradleUtil in the Studio plugin:
+ // -------------------------------------------------------------------------------
+
+ private static final String GRADLE_PROPERTIES = "gradle-wrapper.properties";
+ private static final String GRADLEW_PROPERTIES_PATH =
+ "gradle" + File.separator + "wrapper" + File.separator + GRADLE_PROPERTIES;
+ private static final String GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME = "distributionUrl";
+
+ @NonNull
+ private static File getGradleWrapperPropertiesFilePath(@NonNull File projectRootDir) {
+ return new File(projectRootDir, GRADLEW_PROPERTIES_PATH);
+ }
+
+ @Nullable
+ public static File findWrapperPropertiesFile(@NonNull File projectRootDir) {
+ File wrapperPropertiesFile = getGradleWrapperPropertiesFilePath(projectRootDir);
+ return wrapperPropertiesFile.isFile() ? wrapperPropertiesFile : null;
+ }
+
+ private static boolean updateGradleDistributionUrl(
+ @NonNull String gradleVersion,
+ @NonNull File propertiesFile) throws IOException {
+ Properties properties = loadGradleWrapperProperties(propertiesFile);
+ String gradleDistributionUrl = getGradleDistributionUrl(gradleVersion, false);
+ String property = properties.getProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME);
+ if (property != null
+ && (property.equals(gradleDistributionUrl) || property
+ .equals(getGradleDistributionUrl(gradleVersion, true)))) {
+ return false;
+ }
+ properties.setProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME, gradleDistributionUrl);
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(propertiesFile);
+ properties.store(out, null);
+ return true;
+ } finally {
+ Closeables.close(out, true);
+ }
+ }
+
+ @NonNull
+ private static Properties loadGradleWrapperProperties(@NonNull File propertiesFile)
+ throws IOException {
+ Properties properties = new Properties();
+ FileInputStream fileInputStream = null;
+ try {
+ fileInputStream = new FileInputStream(propertiesFile);
+ properties.load(fileInputStream);
+ return properties;
+ } finally {
+ Closeables.close(fileInputStream, true);
+ }
+ }
+
+ @NonNull
+ private static String getGradleDistributionUrl(@NonNull String gradleVersion,
+ boolean binOnly) {
+ String suffix = binOnly ? "bin" : "all";
+ return String.format("https://services.gradle.org/distributions/gradle-%1$s-" + suffix
+ + ".zip", gradleVersion);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java
new file mode 100644
index 000000000..1f236fb2b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.google.common.collect.Lists;
+
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableLayout;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+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.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.ui.model.WorkbenchLabelProvider;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Confirmation page to review the actual project export
+ * list and see warning about existing files.
+ *
+ */
+public class ConfirmationPage extends WizardPage {
+
+ private final ProjectSetupBuilder mBuilder;
+ private TableViewer mTableViewer;
+ private Label mModuleDescription1;
+ private Label mModuleDescription2;
+ private Label mModuleDescription3;
+ private Label mProjectRootLabel;
+ private Label mProjectRootWarning;
+ private List<IJavaProject> mOverrideProjects;
+ private boolean mOverrideWarning;
+ private Button mForceOverride;
+
+ public ConfirmationPage(ProjectSetupBuilder builder) {
+ super("ConfirmationPage"); //$NON-NLS-1$
+ mBuilder = builder;
+ setPageComplete(false);
+ setTitle(ExportMessages.PageTitle);
+ setDescription(ExportMessages.PageDescription);
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ initializeDialogUnits(parent);
+ GridData data;
+
+ Composite workArea = new Composite(parent, SWT.NONE);
+ setControl(workArea);
+
+ workArea.setLayout(new GridLayout());
+ workArea.setLayoutData(new GridData(GridData.FILL_BOTH
+ | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL));
+
+ Label title = new Label(workArea, SWT.NONE);
+ title.setText("Please review the export options.");
+
+ Group group = new Group(workArea, SWT.NONE);
+ group.setText("Project root");
+ group.setLayout(new GridLayout());
+ group.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+ mProjectRootLabel = new Label(group, SWT.NONE);
+ mProjectRootLabel.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+ mProjectRootWarning = new Label(group, SWT.NONE);
+ mProjectRootWarning.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+ Group group2 = new Group(workArea, SWT.NONE);
+ group2.setText("Exported Modules");
+ group2.setLayout(new GridLayout());
+ group2.setLayoutData(data = new GridData(SWT.FILL, SWT.FILL, true, true));
+ data.heightHint = 300;
+
+ Table table = new Table(group2, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ mTableViewer = new TableViewer(table);
+ table.setLayout(new TableLayout());
+ table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ mTableViewer.setContentProvider(new IStructuredContentProvider() {
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof ProjectSetupBuilder) {
+ ProjectSetupBuilder builder = (ProjectSetupBuilder) inputElement;
+ Collection<GradleModule> modules = builder.getModules();
+ Object[] array = new Object[modules.size()];
+ int i = 0;
+ for (GradleModule module : modules) {
+ array[i++] = module.getJavaProject();
+ }
+
+ return array;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ });
+ mTableViewer.setLabelProvider(new WorkbenchLabelProvider() {
+ @Override
+ protected String decorateText(String input, Object element) {
+ if (element instanceof IJavaProject) {
+ IJavaProject javaProject = (IJavaProject) element;
+ StringBuilder sb = new StringBuilder(input);
+ if (!mBuilder.isOriginalProject(javaProject)) {
+ sb.append('*');
+ }
+ // TODO: decorate icon instead?
+ if (mOverrideProjects.contains(javaProject)) {
+ sb.append(" (1 warning)");
+ }
+
+ return sb.toString();
+ }
+
+ return input;
+ }
+ });
+ mTableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+ Object firstElement = selection.getFirstElement();
+ if (firstElement instanceof IJavaProject) {
+ GradleModule module = mBuilder.getModule((IJavaProject) firstElement);
+ if (mBuilder.getOriginalModules().contains(module)) {
+ mModuleDescription1.setText("Exported because selected in previous page.");
+ } else {
+ List<GradleModule> list = mBuilder.getShortestDependencyTo(module);
+ StringBuilder sb = new StringBuilder();
+ for (GradleModule m : list) {
+ if (sb.length() > 0) {
+ sb.append(" > ");
+ }
+ sb.append(m.getJavaProject().getProject().getName());
+ }
+ mModuleDescription1.setText("Dependency chain: " + sb);
+ }
+ mModuleDescription2.setText("Path: " + module.getPath());
+
+ if (mOverrideProjects.contains(module.getJavaProject())) {
+ mModuleDescription3.setText(
+ "WARNING: build.gradle already exists for this project");
+ } else {
+ mModuleDescription3.setText("");
+ }
+ } else {
+ mModuleDescription1.setText("");
+ mModuleDescription2.setText("");
+ mModuleDescription3.setText("");
+ }
+ }
+ });
+
+ mModuleDescription1 = new Label(group2, SWT.NONE);
+ mModuleDescription1.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+ mModuleDescription2 = new Label(group2, SWT.NONE);
+ mModuleDescription2.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+ mModuleDescription3 = new Label(group2, SWT.NONE);
+ mModuleDescription3.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+
+ mForceOverride = new Button(workArea, SWT.CHECK);
+ mForceOverride.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
+ mForceOverride.setText("Force overriding of existing files");
+ mForceOverride.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateEnablement();
+ }
+ });
+
+ setControl(workArea);
+ Dialog.applyDialogFont(parent);
+ }
+
+ /**
+ * Get list of projects which have already a buildfile.
+ *
+ * @param javaProjects list of IJavaProject objects
+ * @return set of project names
+ */
+ private void computeOverride(String commonRoot) {
+ mOverrideProjects = Lists.newArrayList();
+ for (GradleModule module : mBuilder.getModules()) {
+ if (new File(module.getProject().getLocation().toFile(),
+ BuildFileCreator.BUILD_FILE).exists()) {
+ mOverrideProjects.add(module.getJavaProject());
+ }
+ }
+
+ // also check on the root settings.gradle/build.gradle
+ boolean settingsFile = new File(commonRoot, BuildFileCreator.SETTINGS_FILE).exists();
+ boolean buildFile = new File(commonRoot, BuildFileCreator.BUILD_FILE).exists();
+ if (settingsFile && buildFile) {
+ mProjectRootWarning.setText(
+ "WARNING: build.gradle/settings.gradle already exists at this location.");
+ } else if (settingsFile) {
+ mProjectRootWarning.setText(
+ "WARNING: settings.gradle already exists at this location.");
+ } else if (buildFile) {
+ mProjectRootWarning.setText("WARNING: build.gradle already exists at this location.");
+ }
+
+ mOverrideWarning = mOverrideProjects.size() > 0 || settingsFile || buildFile;
+ }
+
+ /**
+ * Enables/disables the finish button on the wizard and displays error messages as needed.
+ */
+ private void updateEnablement() {
+ if (mOverrideWarning && !mForceOverride.getSelection()) {
+ setErrorMessage("Enable overriding of existing files before clicking Finish");
+ mBuilder.setCanGenerate(false);
+ } else {
+ setErrorMessage(null);
+ mBuilder.setCanGenerate(true);
+ }
+ setPageComplete(false);
+ getContainer().updateButtons();
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ mProjectRootWarning.setText("");
+
+ String commonRoot = mBuilder.getCommonRoot().toOSString();
+ computeOverride(commonRoot);
+ mProjectRootLabel.setText(commonRoot);
+ mTableViewer.setInput(mBuilder);
+ mTableViewer.getTable().setFocus();
+ mBuilder.setCanFinish(false);
+ mBuilder.setCanGenerate(true);
+ updateEnablement();
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java
new file mode 100644
index 000000000..c7d6c1748
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import org.eclipse.osgi.util.NLS;
+
+public class ExportMessages extends NLS {
+ private static final String BUNDLE_NAME =
+ "com.android.ide.eclipse.adt.internal.wizards.exportgradle.ExportMessages";//$NON-NLS-1$
+
+ public static String PageTitle;
+ public static String PageDescription;
+ public static String SelectProjects;
+ public static String ConfirmOverwrite;
+ public static String ConfirmOverwriteTitle;
+ public static String CyclicProjectsError;
+ public static String ExportFailedError;
+ public static String SelectAll;
+ public static String DeselectAll;
+ public static String NoProjectsError;
+ public static String StatusMessage;
+ public static String FileStatusMessage;
+ public static String WindowTitle;
+
+ static {
+ // load message values from bundle file
+ NLS.initializeMessages(BUNDLE_NAME, ExportMessages.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties
new file mode 100644
index 000000000..1a6dbb192
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties
@@ -0,0 +1,27 @@
+# Copyright (C) 2013 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.
+
+PageTitle=Generate Gradle Build files
+PageDescription=Generates Gradle build files based on the configuration of the Java projects
+SelectProjects=Select the projects to use to &generate the Gradle buildfiles:
+ConfirmOverwrite=Are you sure you want to overwrite the buildfiles for these projects?
+ConfirmOverwriteTitle=Overwrite Buildfiles?
+CyclicProjectsError=A cycle was detected in the build path of project: {0}
+ExportFailedError=Buildfile export failed: {0}. See the error log for more details.
+SelectAll=&Select All
+DeselectAll=&Deselect All
+NoProjectsError=Select one or more projects to export.
+StatusMessage=Creating Gradle build files...
+FileStatusMessage=Generating build file for {0}...
+WindowTitle=Export \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java
new file mode 100644
index 000000000..6fbe14e42
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import java.io.File;
+
+public class ExportStatus {
+
+ public static enum FileStatus { OK, VCS_FAILURE, IO_FAILURE; }
+
+ private String mMainError = null;
+ private final Multimap<FileStatus, File> mFileStatus = ArrayListMultimap.create();
+
+ void addFileStatus(@NonNull FileStatus status, @NonNull File file) {
+ mFileStatus.put(status, file);
+ }
+
+ boolean hasError() {
+ return mMainError != null ||
+ !mFileStatus.get(FileStatus.VCS_FAILURE).isEmpty() ||
+ !mFileStatus.get(FileStatus.IO_FAILURE).isEmpty();
+ }
+
+ public void setErrorMessage(String error) {
+ mMainError = error;
+ }
+
+ @Nullable
+ public String getErrorMessage() {
+ return mMainError;
+ }
+
+ @NonNull
+ public Multimap<FileStatus, File> getFileStatus() {
+ return mFileStatus;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java
new file mode 100644
index 000000000..bbfadf855
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.android.ide.eclipse.adt.internal.wizards.exportgradle.ExportStatus.FileStatus;
+import com.google.common.collect.Multimap;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Final page to review the result of the export.
+ */
+public class FinalPage extends WizardPage {
+
+ private final ProjectSetupBuilder mBuilder;
+ private ExportStatus mStatus;
+
+ private Text mText;
+
+ public FinalPage(ProjectSetupBuilder builder) {
+ super("FinalPage"); //$NON-NLS-1$
+ mBuilder = builder;
+ setPageComplete(true);
+ setTitle(ExportMessages.PageTitle);
+ setDescription(ExportMessages.PageDescription);
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ initializeDialogUnits(parent);
+
+ mText = new Text(parent, SWT.MULTI | SWT.READ_ONLY);
+ mText.setLayoutData(new GridData(GridData.FILL_BOTH
+ | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL));
+
+ setControl(mText);
+ Dialog.applyDialogFont(parent);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ mStatus = mBuilder.getStatus();
+ mBuilder.setCanFinish(!mStatus.hasError());
+ mBuilder.setCanGenerate(false);
+
+ StringBuilder sb = new StringBuilder();
+ if (mStatus.hasError()) {
+ sb.append("There was an error!").append("\n\n");
+
+ String errorMsg = mStatus.getErrorMessage();
+ if (errorMsg != null) {
+ sb.append(errorMsg);
+ }
+
+ Multimap<FileStatus, File> fileStatusMap = mStatus.getFileStatus();
+ Collection<File> files = fileStatusMap.values();
+ if (files != null) {
+ sb.append("\n\n").append("Error on files:").append('\n');
+ for (File file : files) {
+ sb.append("\n").append(file.getAbsolutePath());
+ }
+ }
+ } else {
+ sb.append("Export successful.\n\n");
+
+ int count = mBuilder.getModuleCount();
+ if (count > 1) {
+ sb.append(String.format("Exported %s modules", count)).append('\n');
+ sb.append(String.format(
+ "Root folder: %s", mBuilder.getCommonRoot().toOSString()));
+ } else {
+ sb.append("Exported project: ").append(mBuilder.getCommonRoot().toOSString());
+ }
+
+ sb.append("\n\n").append("Choose 'Import Non-Android Studio project' in Android Studio").append('\n');
+ sb.append("and select the following file:").append("\n\t");
+
+ File bGradle = new File(
+ mBuilder.getCommonRoot().toFile(), BuildFileCreator.BUILD_FILE);
+ sb.append(bGradle.getAbsolutePath());
+
+ sb.append("\n\n").append("Do NOT import the Eclipse project itself!");
+ }
+
+ mText.setText(sb.toString());
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java
new file mode 100644
index 000000000..8c74187ff
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ui.IExportWizard;
+import org.eclipse.ui.IWorkbench;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+
+public class GradleExportWizard extends Wizard implements IExportWizard {
+
+ private ProjectSetupBuilder mBuilder = new ProjectSetupBuilder();
+
+ private ProjectSelectionPage mFirstPage;
+ private ConfirmationPage mSecondPage;
+ private FinalPage mFinalPage;
+
+ /**
+ * Creates buildfile.
+ */
+ @Override
+ public boolean performFinish() {
+ if (mBuilder.canGenerate()) {
+ generateBuildfiles(mSecondPage);
+ getContainer().showPage(mFinalPage);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void addPages() {
+ addPage(new ImportInsteadPage());
+ mFirstPage = new ProjectSelectionPage(mBuilder);
+ addPage(mFirstPage);
+ mSecondPage = new ConfirmationPage(mBuilder);
+ addPage(mSecondPage);
+ mFinalPage = new FinalPage(mBuilder);
+ addPage(mFinalPage);
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ setWindowTitle(ExportMessages.WindowTitle);
+ setNeedsProgressMonitor(true);
+ }
+
+ @Override
+ public boolean canFinish() {
+ return mBuilder.canFinish() || mBuilder.canGenerate();
+ }
+
+ /**
+ * Converts Eclipse Java projects to Gradle build files. Displays error dialogs.
+ */
+ public boolean generateBuildfiles(final WizardPage page) {
+ IRunnableWithProgress runnable = new IRunnableWithProgress() {
+ @Override
+ public void run(IProgressMonitor pm) throws InterruptedException {
+ Collection<GradleModule> modules = mBuilder.getModules();
+ final int count = modules.size();
+
+ SubMonitor localmonitor = SubMonitor.convert(pm, ExportMessages.StatusMessage,
+ count);
+ BuildFileCreator.createBuildFiles(
+ mBuilder,
+ page.getShell(),
+ localmonitor.newChild(count));
+ }
+ };
+
+ try {
+ getContainer().run(false, false, runnable);
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ } catch (InterruptedException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ }
+ if (page.getErrorMessage() != null) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java
new file mode 100644
index 000000000..684f03b9a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.android.annotations.NonNull;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jdt.core.IJavaProject;
+
+import java.util.List;
+
+/**
+ * A configured Gradle module for export. This includes gradle path, dependency, type, etc...
+ */
+public class GradleModule {
+
+ @NonNull
+ private final IJavaProject mJavaProject;
+
+ private String mPath;
+ private Type mType;
+
+ private final List<GradleModule> mDependencies = Lists.newArrayList();
+
+ public static enum Type { ANDROID, JAVA };
+
+ GradleModule(@NonNull IJavaProject javaProject) {
+ mJavaProject = javaProject;
+ }
+
+ @NonNull
+ public IJavaProject getJavaProject() {
+ return mJavaProject;
+ }
+
+ @NonNull
+ public IProject getProject() {
+ return mJavaProject.getProject();
+ }
+
+ boolean isConfigured() {
+ return mType != null;
+ }
+
+ public void setType(Type type) {
+ mType = type;
+ }
+
+ public Type getType() {
+ return mType;
+ }
+
+ public void addDependency(GradleModule module) {
+ mDependencies.add(module);
+ }
+
+ public List<GradleModule> getDependencies() {
+ return mDependencies;
+ }
+
+ public void setPath(String path) {
+ mPath = path;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ @Override
+ public String toString() {
+ return "GradleModule [mJavaProject=" + mJavaProject + ", mPath=" + mPath + ", mType="
+ + mType + ", mDependencies=" + mDependencies + "]";
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java
new file mode 100644
index 000000000..cff9aca63
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+
+class ImportInsteadPage extends WizardPage {
+ public ImportInsteadPage() {
+ super("importInstead");
+ setTitle("Import Instead?");
+ setDescription("Consider importing directly into Android Studio instead of exporting from Eclipse");
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(1, false));
+
+ CLabel label = new CLabel(container, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, false, 1, 1));
+ label.setText(
+ "Recent versions of Android Studio now support direct import of ADT projects.\n" +
+ "\n" +
+ "There are advantages to importing from Studio instead of exporting from Eclipse:\n" +
+ "- It can replace jars and library projects with Gradle dependencies instead\n" +
+ "- On import, it creates a new copy of the project and changes the project structure\n" +
+ " to the new Gradle directory layout which better supports multiple resource directories.\n" +
+ "- It can merge instrumentation test projects into the same project\n" +
+ "- Android Studio is released more frequently than the ADT plugin, so the import\n" +
+ " mechanism more closely tracks the requirements of Studio Gradle projects.\n" +
+ "\n" +
+ "If you want to preserve your Eclipse directory structure, or if for some reason import\n" +
+ "in Studio doesn't work (please let us know by filing a bug), continue to export from\n" +
+ "Eclipse instead.");
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java
new file mode 100644
index 000000000..81c7a7346
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.ibm.icu.text.MessageFormat;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaModelMarker;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.TableLayout;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+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.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.ui.model.WorkbenchContentProvider;
+import org.eclipse.ui.model.WorkbenchLabelProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays a wizard page that lets the user choose the projects for which to create Gradle build
+ * files.
+ * <p>
+ * Based on {@link org.eclipse.ant.internal.ui.datatransfer.AntBuildfileExportPage}
+ */
+public class ProjectSelectionPage extends WizardPage {
+
+ private final ProjectSetupBuilder mBuilder;
+ private CheckboxTableViewer mTableViewer;
+ private List<IJavaProject> mSelectedJavaProjects = Lists.newArrayList();
+
+ public ProjectSelectionPage(ProjectSetupBuilder builder) {
+ super("GradleExportPage"); //$NON-NLS-1$
+ mBuilder = builder;
+ setPageComplete(false);
+ setTitle(ExportMessages.PageTitle);
+ setDescription(ExportMessages.PageDescription);
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ initializeDialogUnits(parent);
+
+ Composite workArea = new Composite(parent, SWT.NONE);
+ setControl(workArea);
+
+ workArea.setLayout(new GridLayout());
+ workArea.setLayoutData(new GridData(GridData.FILL_BOTH
+ | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL));
+
+ Label title = new Label(workArea, SWT.NONE);
+ title.setText(ExportMessages.SelectProjects);
+
+ Composite listComposite = new Composite(workArea, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ layout.marginWidth = 0;
+ layout.makeColumnsEqualWidth = false;
+ listComposite.setLayout(layout);
+
+ listComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL
+ | GridData.GRAB_VERTICAL | GridData.FILL_BOTH));
+
+ Table table = new Table(listComposite,
+ SWT.CHECK | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ mTableViewer = new CheckboxTableViewer(table);
+ table.setLayout(new TableLayout());
+ GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+ data.heightHint = 300;
+ table.setLayoutData(data);
+ mTableViewer.setContentProvider(new WorkbenchContentProvider() {
+ @Override
+ public Object[] getElements(Object element) {
+ if (element instanceof IJavaProject[]) {
+ return (IJavaProject[]) element;
+ }
+ return null;
+ }
+ });
+ mTableViewer.setLabelProvider(new WorkbenchLabelProvider());
+ mTableViewer.addCheckStateListener(new ICheckStateListener() {
+ @Override
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ if (event.getChecked()) {
+ mSelectedJavaProjects.add((IJavaProject) event.getElement());
+ } else {
+ mSelectedJavaProjects.remove(event.getElement());
+ }
+ updateEnablement();
+ }
+ });
+
+ initializeProjects();
+ createSelectionButtons(listComposite);
+ setControl(workArea);
+ updateEnablement();
+ Dialog.applyDialogFont(parent);
+ }
+
+ /**
+ * Creates select all/deselect all buttons.
+ */
+ private void createSelectionButtons(Composite composite) {
+ Composite buttonsComposite = new Composite(composite, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.marginWidth = 0;
+ layout.marginHeight = 0;
+ buttonsComposite.setLayout(layout);
+
+ buttonsComposite.setLayoutData(new GridData(
+ GridData.VERTICAL_ALIGN_BEGINNING));
+
+ Button selectAll = new Button(buttonsComposite, SWT.PUSH);
+ selectAll.setText(ExportMessages.SelectAll);
+ selectAll.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ for (int i = 0; i < mTableViewer.getTable().getItemCount(); i++) {
+ mSelectedJavaProjects.add((IJavaProject) mTableViewer.getElementAt(i));
+ }
+ mTableViewer.setAllChecked(true);
+ updateEnablement();
+ }
+ });
+ setButtonLayoutData(selectAll);
+
+ Button deselectAll = new Button(buttonsComposite, SWT.PUSH);
+ deselectAll.setText(ExportMessages.DeselectAll);
+ deselectAll.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mSelectedJavaProjects.clear();
+ mTableViewer.setAllChecked(false);
+ updateEnablement();
+ }
+ });
+ setButtonLayoutData(deselectAll);
+ }
+
+ /**
+ * Populates the list with all the eligible projects in the workspace.
+ */
+ private void initializeProjects() {
+ IWorkspaceRoot rootWorkspace = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(rootWorkspace);
+ IJavaProject[] javaProjects;
+ try {
+ javaProjects = javaModel.getJavaProjects();
+ } catch (JavaModelException e) {
+ javaProjects = new IJavaProject[0];
+ }
+ mTableViewer.setInput(javaProjects);
+ // Check any necessary projects
+ if (mSelectedJavaProjects != null) {
+ mTableViewer.setCheckedElements(mSelectedJavaProjects.toArray(
+ new IJavaProject[mSelectedJavaProjects.size()]));
+ }
+ }
+
+ /**
+ * Enables/disables the finish button on the wizard and displays error messages as needed.
+ */
+ private void updateEnablement() {
+ String error = null;
+ try {
+ if (mSelectedJavaProjects.size() == 0) {
+ error = ExportMessages.NoProjectsError;
+ return;
+ }
+
+ List<String> cyclicProjects;
+ try {
+ cyclicProjects = getCyclicProjects(mSelectedJavaProjects);
+ if (cyclicProjects.size() > 0) {
+ error = MessageFormat.format(ExportMessages.CyclicProjectsError,
+ new Object[] { Joiner.on(", ").join(cyclicProjects) }); //$NON-NLS-1$
+ return;
+ }
+
+ error = mBuilder.setProject(mSelectedJavaProjects);
+ if (error != null) {
+ return;
+ }
+
+ } catch (CoreException ignored) {
+ // TODO: do something?
+ }
+ } finally {
+ setErrorMessage(error);
+ setPageComplete(error == null);
+ getContainer().updateButtons();
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ mTableViewer.getTable().setFocus();
+ mBuilder.setCanFinish(false);
+ mBuilder.setCanGenerate(false);
+ }
+ }
+
+ /**
+ * Returns given projects that have cyclic dependencies.
+ *
+ * @param javaProjects list of IJavaProject objects
+ * @return set of project names
+ */
+ private List<String> getCyclicProjects(List<IJavaProject> projects) throws CoreException {
+
+ List<String> cyclicProjects = new ArrayList<String>();
+ for (IJavaProject javaProject : projects) {
+ if (hasCyclicDependency(javaProject)) {
+ cyclicProjects.add(javaProject.getProject().getName());
+ }
+ }
+ return cyclicProjects;
+ }
+
+ /**
+ * Check if given project has a cyclic dependency.
+ * <p>
+ * See {@link org.eclipse.jdt.core.tests.model.ClasspathTests.numberOfCycleMarkers}
+ */
+ private static boolean hasCyclicDependency(IJavaProject javaProject)
+ throws CoreException {
+ IMarker[] markers = javaProject.getProject().findMarkers(
+ IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, false,
+ IResource.DEPTH_ONE);
+ for (IMarker marker : markers) {
+ String cycleAttr = (String) marker
+ .getAttribute(IJavaModelMarker.CYCLE_DETECTED);
+ if (cycleAttr != null && cycleAttr.equals("true")) { //$NON-NLS-1$
+ return true;
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java
new file mode 100644
index 000000000..1fd6b74f6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Class to setup the project and its modules.
+ */
+public class ProjectSetupBuilder {
+
+ private final static class InternalException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ InternalException(String message) {
+ super(message);
+ }
+ }
+
+ private boolean mCanFinish = false;
+ private boolean mCanGenerate = false;
+ private final List<GradleModule> mOriginalModules = Lists.newArrayList();
+ private final Map<IJavaProject, GradleModule> mModules = Maps.newHashMap();
+ private IPath mCommonRoot;
+ private ExportStatus mStatus;
+
+ public ProjectSetupBuilder() {
+
+ }
+
+ public void setCanGenerate(boolean generate) {
+ mCanGenerate = generate;
+ }
+
+ public void setCanFinish(boolean canFinish) {
+ mCanFinish = canFinish;
+ }
+
+ public boolean canFinish() {
+ return mCanFinish;
+ }
+
+ public boolean canGenerate() {
+ return mCanGenerate;
+ }
+
+ public void setStatus(ExportStatus status) {
+ mStatus = status;
+ }
+
+ public ExportStatus getStatus() {
+ return mStatus;
+ }
+
+ @NonNull
+ public String setProject(@NonNull List<IJavaProject> selectedProjects)
+ throws CoreException {
+ mModules.clear();
+
+ // build a list of all projects that must be included. This is in case
+ // some dependencies have not been included in the selected projects. We also include
+ // parent projects so that the full multi-project setup is correct.
+ // Note that if two projects are selected that are not related, both will be added
+ // in the same multi-project anyway.
+ try {
+ for (IJavaProject javaProject : selectedProjects) {
+ GradleModule module;
+
+ if (javaProject.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) {
+ module = processAndroidProject(javaProject);
+ } else {
+ module = processJavaProject(javaProject);
+ }
+
+ mOriginalModules.add(module);
+ }
+
+ Collection<GradleModule> modules = mModules.values();
+ computeRootAndPaths(modules);
+
+ return null;
+ } catch (InternalException e) {
+ return e.getMessage();
+ }
+ }
+
+ @NonNull
+ public Collection<GradleModule> getModules() {
+ return mModules.values();
+ }
+
+ public int getModuleCount() {
+ return mModules.size();
+ }
+
+ @Nullable
+ public IPath getCommonRoot() {
+ return mCommonRoot;
+ }
+
+ @Nullable
+ public GradleModule getModule(IJavaProject javaProject) {
+ return mModules.get(javaProject);
+ }
+
+ public boolean isOriginalProject(@NonNull IJavaProject javaProject) {
+ GradleModule module = mModules.get(javaProject);
+ return mOriginalModules.contains(module);
+ }
+
+ @NonNull
+ public List<GradleModule> getOriginalModules() {
+ return mOriginalModules;
+ }
+
+ @Nullable
+ public List<GradleModule> getShortestDependencyTo(GradleModule module) {
+ return findModule(module, mOriginalModules);
+ }
+
+ @Nullable
+ public List<GradleModule> findModule(GradleModule toFind, GradleModule rootModule) {
+ if (toFind == rootModule) {
+ List<GradleModule> list = Lists.newArrayList();
+ list.add(toFind);
+ return list;
+ }
+
+ List<GradleModule> shortestChain = findModule(toFind, rootModule.getDependencies());
+
+ if (shortestChain != null) {
+ shortestChain.add(0, rootModule);
+ }
+
+ return shortestChain;
+ }
+
+ @Nullable
+ public List<GradleModule> findModule(GradleModule toFind, List<GradleModule> modules) {
+ List<GradleModule> currentChain = null;
+
+ for (GradleModule child : modules) {
+ List<GradleModule> newChain = findModule(toFind, child);
+ if (currentChain == null) {
+ currentChain = newChain;
+ } else if (newChain != null) {
+ if (currentChain.size() > newChain.size()) {
+ currentChain = newChain;
+ }
+ }
+ }
+
+ return currentChain;
+ }
+
+ @NonNull
+ private GradleModule processAndroidProject(@NonNull IJavaProject javaProject)
+ throws InternalException, CoreException {
+
+ // get/create the module
+ GradleModule module = createModuleOnDemand(javaProject);
+ if (module.isConfigured()) {
+ return module;
+ }
+
+ module.setType(GradleModule.Type.ANDROID);
+
+ ProjectState projectState = Sdk.getProjectState(javaProject.getProject());
+ assert projectState != null;
+
+ // add library project dependencies
+ List<LibraryState> libraryProjects = projectState.getLibraries();
+ for (LibraryState libraryState : libraryProjects) {
+ ProjectState libProjectState = libraryState.getProjectState();
+ if (libProjectState != null) {
+ IJavaProject javaLib = getJavaProject(libProjectState);
+ if (javaLib != null) {
+ GradleModule libModule = processAndroidProject(javaLib);
+ module.addDependency(libModule);
+ } else {
+ throw new InternalException(String.format(
+ "Project %1$s is missing. Needed by %2$s.\n" +
+ "Make sure all dependencies are opened.",
+ libraryState.getRelativePath(),
+ javaProject.getProject().getName()));
+ }
+ } else {
+ throw new InternalException(String.format(
+ "Project %1$s is missing. Needed by %2$s.\n" +
+ "Make sure all dependencies are opened.",
+ libraryState.getRelativePath(),
+ javaProject.getProject().getName()));
+ }
+ }
+
+ // add java project dependencies
+ List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject);
+ for (IJavaProject javaDep : javaDepProjects) {
+ GradleModule libModule = processJavaProject(javaDep);
+ module.addDependency(libModule);
+ }
+
+ return module;
+ }
+
+ @NonNull
+ private GradleModule processJavaProject(@NonNull IJavaProject javaProject)
+ throws InternalException, CoreException {
+ // get/create the module
+ GradleModule module = createModuleOnDemand(javaProject);
+
+ if (module.isConfigured()) {
+ return module;
+ }
+
+ module.setType(GradleModule.Type.JAVA);
+
+ // add java project dependencies
+ List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject);
+ for (IJavaProject javaDep : javaDepProjects) {
+ // Java project should not reference Android project!
+ if (javaDep.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) {
+ throw new InternalException(String.format(
+ "Java project %1$s depends on Android project %2$s!\n" +
+ "This is not a valid dependency",
+ javaProject.getProject().getName(), javaDep.getProject().getName()));
+ }
+ GradleModule libModule = processJavaProject(javaDep);
+ module.addDependency(libModule);
+ }
+
+ return module;
+ }
+
+ private void computeRootAndPaths(Collection<GradleModule> modules) throws InternalException {
+ // compute the common root.
+ mCommonRoot = determineCommonRoot(modules);
+
+ // compute all the relative paths.
+ for (GradleModule module : modules) {
+ String path = getGradlePath(module.getJavaProject().getProject().getLocation(),
+ mCommonRoot);
+
+ module.setPath(path);
+ }
+ }
+
+ /**
+ * Finds the common parent directory shared by this project and all its dependencies.
+ * If there's only one project, returns the single project's folder.
+ * @throws InternalException
+ */
+ @NonNull
+ private static IPath determineCommonRoot(Collection<GradleModule> modules)
+ throws InternalException {
+ IPath commonRoot = null;
+ for (GradleModule module : modules) {
+ if (commonRoot == null) {
+ commonRoot = module.getJavaProject().getProject().getLocation();
+ } else {
+ commonRoot = findCommonRoot(commonRoot,
+ module.getJavaProject().getProject().getLocation());
+ }
+ }
+
+ return commonRoot;
+ }
+
+ /**
+ * Converts the given path to be relative to the given root path, and converts it to
+ * Gradle project notation, such as is used in the settings.gradle file.
+ */
+ @NonNull
+ private static String getGradlePath(IPath path, IPath root) {
+ IPath relativePath = path.makeRelativeTo(root);
+ String relativeString = relativePath.toOSString();
+ return ":" + relativeString.replaceAll(Pattern.quote(File.separator), ":"); //$NON-NLS-1$
+ }
+
+ /**
+ * Given two IPaths, finds the parent directory of both of them.
+ * @throws InternalException
+ */
+ @NonNull
+ private static IPath findCommonRoot(@NonNull IPath path1, @NonNull IPath path2)
+ throws InternalException {
+ if (path1.getDevice() != null && !path1.getDevice().equals(path2.getDevice())) {
+ throw new InternalException(
+ "Different modules have been detected on different drives.\n" +
+ "This prevents finding a common root to all modules.");
+ }
+
+ IPath result = path1.uptoSegment(0);
+
+ final int count = Math.min(path1.segmentCount(), path2.segmentCount());
+ for (int i = 0; i < count; i++) {
+ if (path1.segment(i).equals(path2.segment(i))) {
+ result = result.append(Path.SEPARATOR + path2.segment(i));
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private IJavaProject getJavaProject(ProjectState projectState) {
+ try {
+ return BaseProjectHelper.getJavaProject(projectState.getProject());
+ } catch (CoreException e) {
+ return null;
+ }
+ }
+
+ @NonNull
+ private GradleModule createModuleOnDemand(@NonNull IJavaProject javaProject) {
+ GradleModule module = mModules.get(javaProject);
+ if (module == null) {
+ module = new GradleModule(javaProject);
+ mModules.put(javaProject, module);
+ }
+
+ return module;
+ }
+
+ @NonNull
+ private static List<IJavaProject> getReferencedProjects(IJavaProject javaProject)
+ throws JavaModelException, InternalException {
+
+ List<IJavaProject> projects = Lists.newArrayList();
+
+ IClasspathEntry entries[] = javaProject.getRawClasspath();
+ for (IClasspathEntry classpathEntry : entries) {
+ if (classpathEntry.getContentKind() == IPackageFragmentRoot.K_SOURCE
+ && classpathEntry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
+ // found required project on build path
+ String subProjectRoot = classpathEntry.getPath().toString();
+ IJavaProject subProject = getJavaProject(subProjectRoot);
+ // is project available in workspace?
+ if (subProject != null) {
+ projects.add(subProject);
+ } else {
+ throw new InternalException(String.format(
+ "Project '%s' is missing project dependency '%s' in Eclipse workspace.\n" +
+ "Make sure all dependencies are opened.",
+ javaProject.getProject().getName(),
+ classpathEntry.getPath().toString()));
+ }
+ }
+ }
+
+ return projects;
+ }
+
+ /**
+ * Get Java project for given root.
+ */
+ @Nullable
+ private static IJavaProject getJavaProject(String root) {
+ IPath path = new Path(root);
+ if (path.segmentCount() == 1) {
+ return getJavaProjectByName(root);
+ }
+ IResource resource = ResourcesPlugin.getWorkspace().getRoot()
+ .findMember(path);
+ if (resource != null && resource.getType() == IResource.PROJECT) {
+ if (resource.exists()) {
+ return (IJavaProject) JavaCore.create(resource);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get Java project from resource.
+ */
+ private static IJavaProject getJavaProjectByName(String name) {
+ try {
+ IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
+ if (project.exists()) {
+ return JavaCore.create(project);
+ }
+ } catch (IllegalArgumentException iae) {
+ }
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java
new file mode 100644
index 000000000..c8325345a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.filesystem.URIUtil;
+import org.eclipse.core.resources.IProject;
+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.Status;
+import org.eclipse.jdt.core.JavaConventions;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.URI;
+
+/** Page where you choose the application name, activity name, and optional test project info */
+public class ApplicationInfoPage extends WizardPage implements SelectionListener, ModifyListener,
+ ITargetChangeListener {
+ private static final String JDK_15 = "1.5"; //$NON-NLS-1$
+ private final static String DUMMY_PACKAGE = "your.package.namespace";
+
+ /** Suffix added by default to activity names */
+ static final String ACTIVITY_NAME_SUFFIX = "Activity"; //$NON-NLS-1$
+
+ private final NewProjectWizardState mValues;
+
+ private Text mApplicationText;
+ private Text mPackageText;
+ private Text mActivityText;
+ private Button mCreateActivityCheckbox;
+ private Combo mSdkCombo;
+
+ private boolean mIgnore;
+ private Button mCreateTestCheckbox;
+ private Text mTestProjectNameText;
+ private Text mTestApplicationText;
+ private Text mTestPackageText;
+ private Label mTestProjectNameLabel;
+ private Label mTestApplicationLabel;
+ private Label mTestPackageLabel;
+
+ /**
+ * Create the wizard.
+ */
+ ApplicationInfoPage(NewProjectWizardState values) {
+ super("appInfo"); //$NON-NLS-1$
+ mValues = values;
+
+ setTitle("Application Info");
+ setDescription("Configure the new Android Project");
+ AdtPlugin.getDefault().addTargetListener(this);
+ }
+
+ /**
+ * Create contents of the wizard.
+ */
+ @Override
+ @SuppressWarnings("unused") // Eclipse marks SWT constructors with side effects as unused
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ container.setLayout(new GridLayout(2, false));
+
+ Label applicationLabel = new Label(container, SWT.NONE);
+ applicationLabel.setText("Application Name:");
+
+ mApplicationText = new Text(container, SWT.BORDER);
+ mApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mApplicationText.addModifyListener(this);
+
+ Label packageLabel = new Label(container, SWT.NONE);
+ packageLabel.setText("Package Name:");
+
+ mPackageText = new Text(container, SWT.BORDER);
+ mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mPackageText.addModifyListener(this);
+
+ if (mValues.mode != Mode.TEST) {
+ mCreateActivityCheckbox = new Button(container, SWT.CHECK);
+ mCreateActivityCheckbox.setText("Create Activity:");
+ mCreateActivityCheckbox.addSelectionListener(this);
+
+ mActivityText = new Text(container, SWT.BORDER);
+ mActivityText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mActivityText.addModifyListener(this);
+ }
+
+ Label minSdkLabel = new Label(container, SWT.NONE);
+ minSdkLabel.setText("Minimum SDK:");
+
+ mSdkCombo = new Combo(container, SWT.NONE);
+ GridData gdSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdSdkCombo.widthHint = 200;
+ mSdkCombo.setLayoutData(gdSdkCombo);
+ mSdkCombo.addSelectionListener(this);
+ mSdkCombo.addModifyListener(this);
+
+ onSdkLoaded();
+
+ setControl(container);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+
+ mCreateTestCheckbox = new Button(container, SWT.CHECK);
+ mCreateTestCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ mCreateTestCheckbox.setText("Create a Test Project");
+ mCreateTestCheckbox.addSelectionListener(this);
+
+ mTestProjectNameLabel = new Label(container, SWT.NONE);
+ mTestProjectNameLabel.setText("Test Project Name:");
+
+ mTestProjectNameText = new Text(container, SWT.BORDER);
+ mTestProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mTestProjectNameText.addModifyListener(this);
+
+ mTestApplicationLabel = new Label(container, SWT.NONE);
+ mTestApplicationLabel.setText("Test Application:");
+
+ mTestApplicationText = new Text(container, SWT.BORDER);
+ mTestApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mTestApplicationText.addModifyListener(this);
+
+ mTestPackageLabel = new Label(container, SWT.NONE);
+ mTestPackageLabel.setText("Test Package:");
+
+ mTestPackageText = new Text(container, SWT.BORDER);
+ mTestPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mTestPackageText.addModifyListener(this);
+ }
+
+ /** Controls whether the options for creating a paired test project should be shown */
+ private void showTestOptions(boolean visible) {
+ if (mValues.mode == Mode.SAMPLE) {
+ visible = false;
+ }
+
+ mCreateTestCheckbox.setVisible(visible);
+ mTestProjectNameLabel.setVisible(visible);
+ mTestProjectNameText.setVisible(visible);
+ mTestApplicationLabel.setVisible(visible);
+ mTestApplicationText.setVisible(visible);
+ mTestPackageLabel.setVisible(visible);
+ mTestPackageText.setVisible(visible);
+ }
+
+ /** Controls whether the options for creating a paired test project should be enabled */
+ private void enableTestOptions(boolean enabled) {
+ mTestProjectNameLabel.setEnabled(enabled);
+ mTestProjectNameText.setEnabled(enabled);
+ mTestApplicationLabel.setEnabled(enabled);
+ mTestApplicationText.setEnabled(enabled);
+ mTestPackageLabel.setEnabled(enabled);
+ mTestPackageText.setEnabled(enabled);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ if (visible) {
+ try {
+ mIgnore = true;
+ if (mValues.applicationName != null) {
+ mApplicationText.setText(mValues.applicationName);
+ }
+ if (mValues.packageName != null) {
+ mPackageText.setText(mValues.packageName);
+ } else {
+ mPackageText.setText(DUMMY_PACKAGE);
+ }
+
+ if (mValues.mode != Mode.TEST) {
+ mCreateActivityCheckbox.setSelection(mValues.createActivity);
+ mActivityText.setEnabled(mValues.createActivity);
+ if (mValues.activityName != null) {
+ mActivityText.setText(mValues.activityName);
+ }
+ }
+ if (mValues.minSdk != null && mValues.minSdk.length() > 0) {
+ mSdkCombo.setText(mValues.minSdk);
+ }
+
+ showTestOptions(mValues.mode == Mode.ANY);
+ enableTestOptions(mCreateTestCheckbox.getSelection());
+
+ if (mValues.testProjectName != null) {
+ mTestProjectNameText.setText(mValues.testProjectName);
+ }
+ if (mValues.testApplicationName != null) {
+ mTestApplicationText.setText(mValues.testApplicationName);
+ }
+ if (mValues.testProjectName != null) {
+ mTestPackageText.setText(mValues.testProjectName);
+ }
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ // Start focus with the package name, since the other fields are typically assigned
+ // reasonable defaults
+ mPackageText.setFocus();
+ mPackageText.selectAll();
+
+ validatePage();
+ }
+
+ protected void setSdkTargets(IAndroidTarget[] targets, IAndroidTarget target) {
+ if (targets == null) {
+ targets = new IAndroidTarget[0];
+ }
+ int selectionIndex = -1;
+ String[] items = new String[targets.length];
+ for (int i = 0, n = targets.length; i < n; i++) {
+ items[i] = targetLabel(targets[i]);
+ if (targets[i] == target) {
+ selectionIndex = i;
+ }
+ }
+ try {
+ mIgnore = true;
+ mSdkCombo.setItems(items);
+ mSdkCombo.setData(targets);
+ if (selectionIndex != -1) {
+ mSdkCombo.select(selectionIndex);
+ }
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ private String targetLabel(IAndroidTarget target) {
+ // In the minimum SDK chooser, show the targets with api number and description,
+ // such as "11 (Android 3.0)"
+ return String.format("%1$s (%2$s)", target.getVersion().getApiString(),
+ target.getFullName());
+ }
+
+ @Override
+ public void dispose() {
+ AdtPlugin.getDefault().removeTargetListener(this);
+ super.dispose();
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ // This page is only needed when creating new projects
+ if (mValues.useExisting || mValues.mode != Mode.ANY) {
+ return true;
+ }
+
+ // Ensure that we reach this page
+ if (mValues.packageName == null) {
+ return false;
+ }
+
+ return super.isPageComplete();
+ }
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mSdkCombo) {
+ mValues.minSdk = mSdkCombo.getText().trim();
+ IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData();
+ // An editable combo will treat item selection the same way as a user edit,
+ // so we need to see if the string looks like a labeled version
+ int index = mSdkCombo.getSelectionIndex();
+ if (index != -1) {
+ if (index >= 0 && index < targets.length) {
+ IAndroidTarget target = targets[index];
+ if (targetLabel(target).equals(mValues.minSdk)) {
+ mValues.minSdk = target.getVersion().getApiString();
+ }
+ }
+ }
+
+ // Ensure that we never pick up the (Android x.y) suffix shown in combobox
+ // for readability
+ int separator = mValues.minSdk.indexOf(' ');
+ if (separator != -1) {
+ mValues.minSdk = mValues.minSdk.substring(0, separator);
+ }
+ mValues.minSdkModifiedByUser = true;
+ mValues.updateSdkTargetToMatchMinSdkVersion();
+ } else if (source == mApplicationText) {
+ mValues.applicationName = mApplicationText.getText().trim();
+ mValues.applicationNameModifiedByUser = true;
+
+ if (!mValues.testApplicationNameModified) {
+ mValues.testApplicationName = suggestTestApplicationName(mValues.applicationName);
+ try {
+ mIgnore = true;
+ mTestApplicationText.setText(mValues.testApplicationName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ } else if (source == mPackageText) {
+ mValues.packageName = mPackageText.getText().trim();
+ mValues.packageNameModifiedByUser = true;
+
+ if (!mValues.testPackageModified) {
+ mValues.testPackageName = suggestTestPackage(mValues.packageName);
+ try {
+ mIgnore = true;
+ mTestPackageText.setText(mValues.testPackageName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ } else if (source == mActivityText) {
+ mValues.activityName = mActivityText.getText().trim();
+ mValues.activityNameModifiedByUser = true;
+ } else if (source == mTestApplicationText) {
+ mValues.testApplicationName = mTestApplicationText.getText().trim();
+ mValues.testApplicationNameModified = true;
+ } else if (source == mTestPackageText) {
+ mValues.testPackageName = mTestPackageText.getText().trim();
+ mValues.testPackageModified = true;
+ } else if (source == mTestProjectNameText) {
+ mValues.testProjectName = mTestProjectNameText.getText().trim();
+ mValues.testProjectModified = true;
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+
+ if (source == mCreateActivityCheckbox) {
+ mValues.createActivity = mCreateActivityCheckbox.getSelection();
+ mActivityText.setEnabled(mValues.createActivity);
+ } else if (source == mSdkCombo) {
+ int index = mSdkCombo.getSelectionIndex();
+ IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData();
+ if (index != -1) {
+ if (index >= 0 && index < targets.length) {
+ IAndroidTarget target = targets[index];
+ // Even though we are showing the logical version name, we place the
+ // actual api number as the minimum SDK
+ mValues.minSdk = target.getVersion().getApiString();
+ }
+ } else {
+ String text = mSdkCombo.getText();
+ boolean found = false;
+ for (IAndroidTarget target : targets) {
+ if (targetLabel(target).equals(text)) {
+ mValues.minSdk = target.getVersion().getApiString();
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ mValues.minSdk = text;
+ }
+ }
+ } else if (source == mCreateTestCheckbox) {
+ mValues.createPairProject = mCreateTestCheckbox.getSelection();
+ enableTestOptions(mValues.createPairProject);
+ if (mValues.createPairProject) {
+ if (mValues.testProjectName == null || mValues.testProjectName.length() == 0) {
+ mValues.testProjectName = suggestTestProjectName(mValues.projectName);
+ }
+ if (mValues.testApplicationName == null ||
+ mValues.testApplicationName.length() == 0) {
+ mValues.testApplicationName =
+ suggestTestApplicationName(mValues.applicationName);
+ }
+ if (mValues.testPackageName == null || mValues.testPackageName.length() == 0) {
+ mValues.testPackageName = suggestTestPackage(mValues.packageName);
+ }
+
+ try {
+ mIgnore = true;
+ mTestProjectNameText.setText(mValues.testProjectName);
+ mTestApplicationText.setText(mValues.testApplicationName);
+ mTestPackageText.setText(mValues.testPackageName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ private void validatePage() {
+ IStatus status = validatePackage(mValues.packageName);
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validActivity = validateActivity();
+ if (validActivity != null) {
+ status = validActivity;
+ }
+ }
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validMinSdk = validateMinSdk();
+ if (validMinSdk != null) {
+ status = validMinSdk;
+ }
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validSourceFolder = validateSourceFolder();
+ if (validSourceFolder != null) {
+ status = validSourceFolder;
+ }
+ }
+
+ // If creating a test project to go along with the main project, also validate
+ // the additional test project parameters
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.createPairProject) {
+ IStatus validTestProject = ProjectNamePage.validateProjectName(
+ mValues.testProjectName);
+ if (validTestProject != null) {
+ status = validTestProject;
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validTestLocation = validateTestProjectLocation();
+ if (validTestLocation != null) {
+ status = validTestLocation;
+ }
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validTestPackage = validatePackage(mValues.testPackageName);
+ if (validTestPackage != null) {
+ status = new Status(validTestPackage.getSeverity(),
+ AdtPlugin.PLUGIN_ID,
+ validTestPackage.getMessage() + " (in test package)");
+ }
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.projectName.equals(mValues.testProjectName)) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "The main project name and the test project name must be different.");
+ }
+ }
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ private IStatus validateTestProjectLocation() {
+ assert mValues.createPairProject;
+
+ // Validate location
+ Path path = new Path(mValues.projectLocation.getPath());
+ if (!mValues.useExisting) {
+ if (!mValues.useDefaultLocation) {
+ // If not using the default value validate the location.
+ URI uri = URIUtil.toURI(path.toOSString());
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IProject handle = workspace.getRoot().getProject(mValues.testProjectName);
+ IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri);
+ if (!locationStatus.isOK()) {
+ return locationStatus;
+ }
+ // 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 new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "A directory name must be specified.");
+ } 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 != null && l.length != 0) {
+ return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "The selected output directory is not empty.");
+ }
+ }
+ } else {
+ IPath destPath = path.removeLastSegments(1).append(mValues.testProjectName);
+ File dest = destPath.toFile();
+ if (dest.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "There is already a file or directory named \"%1$s\" in the selected location.",
+ mValues.testProjectName));
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private IStatus validateSourceFolder() {
+ // This check does nothing when creating a new project.
+ // This check is also useless when no activity is present or created.
+ mValues.sourceFolder = SdkConstants.FD_SOURCES;
+ if (!mValues.useExisting || !mValues.createActivity) {
+ return null;
+ }
+
+ String osTarget = mValues.activityName;
+ if (osTarget.indexOf('.') == -1) {
+ osTarget = mValues.packageName + File.separator + osTarget;
+ } else if (osTarget.indexOf('.') == 0) {
+ osTarget = mValues.packageName + osTarget;
+ }
+ osTarget = osTarget.replace('.', File.separatorChar) + SdkConstants.DOT_JAVA;
+
+ File projectDir = mValues.projectLocation;
+ File[] allDirs = projectDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.isDirectory();
+ }
+ });
+ if (allDirs != null) {
+ boolean found = false;
+ for (File f : allDirs) {
+ Path path = new Path(f.getAbsolutePath());
+ File java_activity = path.append(osTarget).toFile();
+ if (java_activity.isFile()) {
+ mValues.sourceFolder = f.getName();
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ String projectPath = projectDir.getPath();
+ if (allDirs.length > 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("%1$s can not be found under %2$s.", osTarget,
+ projectPath));
+ } else {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("No source folders can be found in %1$s.",
+ projectPath));
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private IStatus validateMinSdk() {
+ // Validate min SDK field
+ // If the min sdk version is empty, it is always accepted.
+ if (mValues.minSdk == null || mValues.minSdk.length() == 0) {
+ return null;
+ }
+
+ IAndroidTarget target = mValues.target;
+ if (target == null) {
+ return null;
+ }
+
+ // If the current target is a preview, explicitly indicate minSdkVersion
+ // must be set to this target name.
+ if (target.getVersion().isPreview() && !target.getVersion().equals(mValues.minSdk)) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "The SDK target is a preview. Min SDK Version must be set to '%s'.",
+ target.getVersion().getCodename()));
+ }
+
+ if (!target.getVersion().equals(mValues.minSdk)) {
+ return new Status(target.getVersion().isPreview() ? IStatus.ERROR : IStatus.WARNING,
+ AdtPlugin.PLUGIN_ID,
+ "The API level for the selected SDK target does not match the Min SDK Version."
+ );
+ }
+
+ return null;
+ }
+
+ public static IStatus validatePackage(String packageFieldContents) {
+ // Validate package
+ if (packageFieldContents == null || packageFieldContents.length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Package name must be specified.");
+ } else if (packageFieldContents.equals(DUMMY_PACKAGE)) {
+ // The dummy package name is just a placeholder package (which isn't even valid
+ // because it contains the reserved Java keyword "package") but we want to
+ // make the error message say that a proper package should be entered rather than
+ // what's wrong with this specific package. (And the reason we provide a dummy
+ // package rather than a blank line is to make it more clear to beginners what
+ // we're looking for.
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Package name must be specified.");
+ }
+ // Check it's a valid package string
+ IStatus status = JavaConventions.validatePackageName(packageFieldContents, JDK_15,
+ JDK_15);
+ if (!status.isOK()) {
+ return status;
+ }
+
+ // 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 (packageFieldContents.indexOf('.') == -1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Package name must have at least two identifiers.");
+ }
+
+ return null;
+ }
+
+ public static IStatus validateClass(String className) {
+ if (className == null || className.length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Class name must be specified.");
+ }
+ if (className.indexOf('.') != -1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Enter just a class name, not a full package name");
+ }
+ return JavaConventions.validateJavaTypeName(className, JDK_15, JDK_15);
+ }
+
+ private IStatus validateActivity() {
+ // Validate activity (if creating an activity)
+ if (!mValues.createActivity) {
+ return null;
+ }
+
+ return validateActivity(mValues.activityName);
+ }
+
+ /**
+ * Validates the given activity name
+ *
+ * @param activityFieldContents the activity name to validate
+ * @return a status for whether the activity name is valid
+ */
+ public static IStatus validateActivity(String activityFieldContents) {
+ // Validate activity field
+ if (activityFieldContents == null || activityFieldContents.length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Activity name must be specified.");
+ } else if (ACTIVITY_NAME_SUFFIX.equals(activityFieldContents)) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Enter a valid activity name");
+ } else if (activityFieldContents.contains("..")) { //$NON-NLS-1$
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Package segments in activity name cannot be empty (..)");
+ }
+ // 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 = ""; //$NON-NLS-1$
+ 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.length() > 0 && activityFieldContents.charAt(0) == '.') {
+ activityFieldContents = activityFieldContents.substring(1);
+ }
+
+ // Check it's a valid activity string
+ IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents, JDK_15,
+ JDK_15);
+ if (!status.isOK()) {
+ return status;
+ }
+
+ // Check it's a valid package string
+ if (packageName.length() > 0) {
+ status = JavaConventions.validatePackageName(packageName, JDK_15, JDK_15);
+ if (!status.isOK()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ status.getMessage() + " (in the activity name)");
+ }
+ }
+
+ return null;
+ }
+
+ // ---- Implement ITargetChangeListener ----
+
+ @Override
+ public void onSdkLoaded() {
+ if (mSdkCombo == null) {
+ return;
+ }
+
+ // 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();
+ }
+ setSdkTargets(targets, mValues.target);
+ }
+
+ @Override
+ public void onProjectTargetChange(IProject changedProject) {
+ // Ignore
+ }
+
+ @Override
+ public void onTargetLoaded(IAndroidTarget target) {
+ // Ignore
+ }
+
+ public static String suggestTestApplicationName(String applicationName) {
+ if (applicationName == null) {
+ applicationName = ""; //$NON-NLS-1$
+ }
+ if (applicationName.indexOf(' ') != -1) {
+ return applicationName + " Test"; //$NON-NLS-1$
+ } else {
+ return applicationName + "Test"; //$NON-NLS-1$
+ }
+ }
+
+ public static String suggestTestProjectName(String projectName) {
+ if (projectName == null) {
+ projectName = ""; //$NON-NLS-1$
+ }
+ if (projectName.length() > 0 && Character.isUpperCase(projectName.charAt(0))) {
+ return projectName + "Test"; //$NON-NLS-1$
+ } else {
+ return projectName + "-test"; //$NON-NLS-1$
+ }
+ }
+
+
+ public static String suggestTestPackage(String packagePath) {
+ if (packagePath == null) {
+ packagePath = ""; //$NON-NLS-1$
+ }
+ return packagePath + ".test"; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java
new file mode 100755
index 000000000..0f4e87e9f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import org.eclipse.core.filesystem.IFileInfo;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.filesystem.IFileSystem;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+
+/**
+ * IFileStore implementation that delegates to the give {@link IFileStore}.
+ * This makes it easier to just override a single method from a store.
+ */
+class FileStoreAdapter implements IFileStore {
+
+ private final IFileStore mStore;
+
+ public FileStoreAdapter(IFileStore store) {
+ mStore = store;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Object getAdapter(Class adapter) {
+ return mStore.getAdapter(adapter);
+ }
+
+ @Override
+ public IFileInfo[] childInfos(int options, IProgressMonitor monitor) throws CoreException {
+ return mStore.childInfos(options, monitor);
+ }
+
+ @Override
+ public String[] childNames(int options, IProgressMonitor monitor)
+ throws CoreException {
+ return mStore.childNames(options, monitor);
+ }
+
+ @Override
+ public IFileStore[] childStores(int options, IProgressMonitor monitor) throws CoreException {
+ return mStore.childStores(options, monitor);
+ }
+
+ @Override
+ public void copy(IFileStore destination, int options, IProgressMonitor monitor)
+ throws CoreException {
+ mStore.copy(destination, options, monitor);
+ }
+
+ @Override
+ public void delete(int options, IProgressMonitor monitor) throws CoreException {
+ mStore.delete(options, monitor);
+ }
+
+ @Override
+ public IFileInfo fetchInfo() {
+ return mStore.fetchInfo();
+ }
+
+ @Override
+ public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException {
+ return mStore.fetchInfo(options, monitor);
+ }
+
+ @Deprecated
+ @Override
+ public IFileStore getChild(IPath path) {
+ return mStore.getChild(path);
+ }
+
+ @Override
+ public IFileStore getFileStore(IPath path) {
+ return mStore.getFileStore(path);
+ }
+
+ @Override
+ public IFileStore getChild(String name) {
+ return mStore.getChild(name);
+ }
+
+ @Override
+ public IFileSystem getFileSystem() {
+ return mStore.getFileSystem();
+ }
+
+ @Override
+ public String getName() {
+ return mStore.getName();
+ }
+
+ @Override
+ public IFileStore getParent() {
+ return mStore.getParent();
+ }
+
+ @Override
+ public boolean isParentOf(IFileStore other) {
+ return mStore.isParentOf(other);
+ }
+
+ @Override
+ public IFileStore mkdir(int options, IProgressMonitor monitor) throws CoreException {
+ return mStore.mkdir(options, monitor);
+ }
+
+ @Override
+ public void move(IFileStore destination, int options, IProgressMonitor monitor)
+ throws CoreException {
+ mStore.move(destination, options, monitor);
+ }
+
+ @Override
+ public InputStream openInputStream(int options, IProgressMonitor monitor)
+ throws CoreException {
+ return mStore.openInputStream(options, monitor);
+ }
+
+ @Override
+ public OutputStream openOutputStream(int options, IProgressMonitor monitor)
+ throws CoreException {
+ return mStore.openOutputStream(options, monitor);
+ }
+
+ @Override
+ public void putInfo(IFileInfo info, int options, IProgressMonitor monitor)
+ throws CoreException {
+ mStore.putInfo(info, options, monitor);
+ }
+
+ @Override
+ public File toLocalFile(int options, IProgressMonitor monitor) throws CoreException {
+ return mStore.toLocalFile(options, monitor);
+ }
+
+ @Override
+ public URI toURI() {
+ return mStore.toURI();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java
new file mode 100644
index 000000000..1e02fedae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.tools.lint.detector.api.LintUtils;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ColumnViewer;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Rectangle;
+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.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkingSet;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** WizardPage for importing Android projects */
+class ImportPage extends WizardPage implements SelectionListener, IStructuredContentProvider,
+ ICheckStateListener, KeyListener, TraverseListener, ControlListener {
+ private static final int DIR_COLUMN = 0;
+ private static final int NAME_COLUMN = 1;
+
+ private final NewProjectWizardState mValues;
+ private List<ImportedProject> mProjectPaths;
+ private final IProject[] mExistingProjects;
+
+ private Text mDir;
+ private Button mBrowseButton;
+ private Button mCopyCheckBox;
+ private Button mRefreshButton;
+ private Button mDeselectAllButton;
+ private Button mSelectAllButton;
+ private Table mTable;
+ private CheckboxTableViewer mCheckboxTableViewer;
+ private WorkingSetGroup mWorkingSetGroup;
+
+ ImportPage(NewProjectWizardState values) {
+ super("importPage"); //$NON-NLS-1$
+ mValues = values;
+ setTitle("Import Projects");
+ setDescription("Select a directory to search for existing Android projects");
+ mWorkingSetGroup = new WorkingSetGroup();
+ setWorkingSets(new IWorkingSet[0]);
+
+ // Record all projects such that we can ensure that the project names are unique
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ mExistingProjects = workspaceRoot.getProjects();
+ }
+
+ public void init(IStructuredSelection selection, IWorkbenchPart activePart) {
+ setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(3, false));
+
+ Label directoryLabel = new Label(container, SWT.NONE);
+ directoryLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ directoryLabel.setText("Root Directory:");
+
+ mDir = new Text(container, SWT.BORDER);
+ mDir.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mDir.addKeyListener(this);
+ mDir.addTraverseListener(this);
+
+ mBrowseButton = new Button(container, SWT.NONE);
+ mBrowseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ mBrowseButton.setText("Browse...");
+ mBrowseButton.addSelectionListener(this);
+
+ Label projectsLabel = new Label(container, SWT.NONE);
+ projectsLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+ projectsLabel.setText("Projects:");
+
+ mTable = new Table(container, SWT.CHECK);
+ mTable.setHeaderVisible(true);
+ mCheckboxTableViewer = new CheckboxTableViewer(mTable);
+
+ TableViewerColumn dirViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE);
+ TableColumn dirColumn = dirViewerColumn.getColumn();
+ dirColumn.setWidth(200);
+ dirColumn.setText("Project to Import");
+ TableViewerColumn nameViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE);
+ TableColumn nameColumn = nameViewerColumn.getColumn();
+ nameColumn.setWidth(200);
+ nameColumn.setText("New Project Name");
+ nameViewerColumn.setEditingSupport(new ProjectNameEditingSupport(mCheckboxTableViewer));
+
+ mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 4));
+ mTable.setLinesVisible(true);
+ mTable.setHeaderVisible(true);
+ mTable.addSelectionListener(this);
+ mTable.addControlListener(this);
+ mCheckboxTableViewer.setContentProvider(this);
+ mCheckboxTableViewer.setInput(this);
+ mCheckboxTableViewer.addCheckStateListener(this);
+ mCheckboxTableViewer.setLabelProvider(new ProjectCellLabelProvider());
+
+ mSelectAllButton = new Button(container, SWT.NONE);
+ mSelectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ mSelectAllButton.setText("Select All");
+ mSelectAllButton.addSelectionListener(this);
+
+ mDeselectAllButton = new Button(container, SWT.NONE);
+ mDeselectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ mDeselectAllButton.setText("Deselect All");
+ mDeselectAllButton.addSelectionListener(this);
+
+ mRefreshButton = new Button(container, SWT.NONE);
+ mRefreshButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ mRefreshButton.setText("Refresh");
+ mRefreshButton.addSelectionListener(this);
+ new Label(container, SWT.NONE);
+
+ mCopyCheckBox = new Button(container, SWT.CHECK);
+ mCopyCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+ mCopyCheckBox.setText("Copy projects into workspace");
+ mCopyCheckBox.addSelectionListener(this);
+
+ Composite group = mWorkingSetGroup.createControl(container);
+ group.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1));
+
+ updateColumnWidths();
+ }
+
+ private void updateColumnWidths() {
+ Rectangle r = mTable.getClientArea();
+ int availableWidth = r.width;
+ // Add all available size to the first column
+ for (int i = 1; i < mTable.getColumnCount(); i++) {
+ TableColumn column = mTable.getColumn(i);
+ availableWidth -= column.getWidth();
+ }
+ if (availableWidth > 100) {
+ mTable.getColumn(0).setWidth(availableWidth);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ validatePage();
+ }
+
+ private void refresh() {
+ File root = new File(mDir.getText().trim());
+ mProjectPaths = searchForProjects(root);
+ mCheckboxTableViewer.refresh();
+ mCheckboxTableViewer.setAllChecked(true);
+
+ updateValidity();
+ validatePage();
+ }
+
+ private void updateValidity(){
+ List<ImportedProject> selected = new ArrayList<ImportedProject>();
+ List<ImportedProject> disabled = new ArrayList<ImportedProject>();
+ for (ImportedProject project : mProjectPaths) {
+ String projectName = project.getProjectName();
+ boolean invalid = false;
+ for (IProject existingProject : mExistingProjects) {
+ if (projectName.equals(existingProject.getName())) {
+ invalid = true;
+ break;
+ }
+ }
+ if (invalid) {
+ disabled.add(project);
+ } else {
+ selected.add(project);
+ }
+ }
+
+ mValues.importProjects = selected;
+
+ mCheckboxTableViewer.setGrayedElements(disabled.toArray());
+ mCheckboxTableViewer.setCheckedElements(selected.toArray());
+ mCheckboxTableViewer.refresh();
+ mCheckboxTableViewer.getTable().setFocus();
+ }
+
+ private List<ImportedProject> searchForProjects(File dir) {
+ List<ImportedProject> projects = new ArrayList<ImportedProject>();
+ addProjects(dir, projects, dir.getPath().length() + 1);
+ return projects;
+ }
+
+ /** Finds all project directories under the given directory */
+ private void addProjects(File dir, List<ImportedProject> projects, int prefixLength) {
+ if (dir.isDirectory()) {
+ if (LintUtils.isManifestFolder(dir)) {
+ String relative = dir.getPath();
+ if (relative.length() > prefixLength) {
+ relative = relative.substring(prefixLength);
+ }
+ projects.add(new ImportedProject(dir, relative));
+ }
+
+ File[] children = dir.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ addProjects(child, projects, prefixLength);
+ }
+ }
+ }
+ }
+
+ private void validatePage() {
+ IStatus status = null;
+
+ // Validate project name -- unless we're creating a sample, in which case
+ // the user will get a chance to pick the name on the Sample page
+ if (mProjectPaths == null || mProjectPaths.isEmpty()) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Select a directory to search for existing Android projects");
+ } else if (mValues.importProjects == null || mValues.importProjects.isEmpty()) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Select at least one project");
+ } else {
+ for (ImportedProject project : mValues.importProjects) {
+ if (mCheckboxTableViewer.getGrayed(project)) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Cannot import %1$s because the project name is in use",
+ project.getProjectName()));
+ break;
+ } else {
+ status = ProjectNamePage.validateProjectName(project.getProjectName());
+ if (status != null && !status.isOK()) {
+ // Need to insert project name to make it clear which project name
+ // is in violation
+ if (mValues.importProjects.size() > 1) {
+ String message = String.format("%1$s: %2$s",
+ project.getProjectName(), status.getMessage());
+ status = new Status(status.getSeverity(), AdtPlugin.PLUGIN_ID,
+ message);
+ }
+ break;
+ } else {
+ status = null; // Don't leave non null status with isOK() == true
+ }
+ }
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ /**
+ * Returns the working sets to which the new project should be added.
+ *
+ * @return the selected working sets to which the new project should be added
+ */
+ private IWorkingSet[] getWorkingSets() {
+ return mWorkingSetGroup.getSelectedWorkingSets();
+ }
+
+ /**
+ * Sets the working sets to which the new project should be added.
+ *
+ * @param workingSets the initial selected working sets
+ */
+ private void setWorkingSets(IWorkingSet[] workingSets) {
+ assert workingSets != null;
+ mWorkingSetGroup.setWorkingSets(workingSets);
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ // Sync working set data to the value object, since the WorkingSetGroup
+ // doesn't let us add listeners to do this lazily
+ mValues.workingSets = getWorkingSets();
+
+ return super.getNextPage();
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+ if (source == mBrowseButton) {
+ // Choose directory
+ DirectoryDialog dialog = new DirectoryDialog(getShell(), SWT.OPEN);
+ String path = mDir.getText().trim();
+ if (path.length() > 0) {
+ dialog.setFilterPath(path);
+ }
+ String file = dialog.open();
+ if (file != null) {
+ mDir.setText(file);
+ refresh();
+ }
+ } else if (source == mSelectAllButton) {
+ mCheckboxTableViewer.setAllChecked(true);
+ mValues.importProjects = mProjectPaths;
+ } else if (source == mDeselectAllButton) {
+ mCheckboxTableViewer.setAllChecked(false);
+ mValues.importProjects = Collections.emptyList();
+ } else if (source == mRefreshButton || source == mDir) {
+ refresh();
+ } else if (source == mCopyCheckBox) {
+ mValues.copyIntoWorkspace = mCopyCheckBox.getSelection();
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- KeyListener ----
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getSource() == mDir) {
+ if (e.keyCode == SWT.CR) {
+ refresh();
+ }
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+
+ // ---- TraverseListener ----
+
+ @Override
+ public void keyTraversed(TraverseEvent e) {
+ // Prevent Return from running through the wizard; return is handled by
+ // key listener to refresh project list instead
+ if (SWT.TRAVERSE_RETURN == e.detail) {
+ e.doit = false;
+ }
+ }
+
+ // ---- Implements IStructuredContentProvider ----
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return mProjectPaths != null ? mProjectPaths.toArray() : new Object[0];
+ }
+
+ // ---- Implements ICheckStateListener ----
+
+ @Override
+ public void checkStateChanged(CheckStateChangedEvent event) {
+ // Try to disable other elements that conflict with this
+ Object[] checked = mCheckboxTableViewer.getCheckedElements();
+ List<ImportedProject> selected = new ArrayList<ImportedProject>(checked.length);
+ for (Object o : checked) {
+ if (!mCheckboxTableViewer.getGrayed(o)) {
+ selected.add((ImportedProject) o);
+ }
+ }
+ mValues.importProjects = selected;
+ validatePage();
+
+ mCheckboxTableViewer.update(event.getElement(), null);
+ }
+
+ // ---- Implements ControlListener ----
+
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ updateColumnWidths();
+ }
+
+ private final class ProjectCellLabelProvider extends CellLabelProvider {
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ int index = cell.getColumnIndex();
+ ImportedProject project = (ImportedProject) element;
+
+ Display display = mTable.getDisplay();
+ Color fg;
+ if (mCheckboxTableViewer.getGrayed(element)) {
+ fg = display.getSystemColor(SWT.COLOR_DARK_GRAY);
+ } else {
+ fg = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+ }
+ cell.setForeground(fg);
+ cell.setBackground(display.getSystemColor(SWT.COLOR_LIST_BACKGROUND));
+
+ switch (index) {
+ case DIR_COLUMN: {
+ // Directory name
+ cell.setText(project.getRelativePath());
+ return;
+ }
+
+ case NAME_COLUMN: {
+ // New name
+ cell.setText(project.getProjectName());
+ return;
+ }
+ default:
+ assert false : index;
+ }
+ cell.setText("");
+ }
+ }
+
+ /** Editing support for the project name column */
+ private class ProjectNameEditingSupport extends EditingSupport {
+ private ProjectNameEditingSupport(ColumnViewer viewer) {
+ super(viewer);
+ }
+
+ @Override
+ protected void setValue(Object element, Object value) {
+ ImportedProject project = (ImportedProject) element;
+ project.setProjectName(value.toString());
+ mCheckboxTableViewer.update(element, null);
+ updateValidity();
+ validatePage();
+ }
+
+ @Override
+ protected Object getValue(Object element) {
+ ImportedProject project = (ImportedProject) element;
+ return project.getProjectName();
+ }
+
+ @Override
+ protected CellEditor getCellEditor(Object element) {
+ return new TextCellEditor(mTable);
+ }
+
+ @Override
+ protected boolean canEdit(Object element) {
+ return true;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java
new file mode 100644
index 000000000..1004fd692
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
+import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+
+import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+
+
+/**
+ * An "Import Android Project" wizard.
+ */
+public class ImportProjectWizard extends Wizard implements INewWizard {
+ private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$
+
+ private NewProjectWizardState mValues;
+ private ImportPage mImportPage;
+ private IStructuredSelection mSelection;
+
+ /** Constructs a new wizard default project wizard */
+ public ImportProjectWizard() {
+ }
+
+ @Override
+ public void addPages() {
+ mValues = new NewProjectWizardState(Mode.ANY);
+ mImportPage = new ImportPage(mValues);
+ if (mSelection != null) {
+ mImportPage.init(mSelection, AdtUtils.getActivePart());
+ }
+ addPage(mImportPage);
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ mSelection = selection;
+
+ setHelpAvailable(false); // TODO have help
+ ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ @Override
+ public boolean performFinish() {
+ File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator
+ + FN_PROJECT_PROGUARD_FILE);
+ if (!file.exists()) {
+ AdtPlugin.displayError("Tools Out of Date?",
+ String.format("It looks like you do not have the latest version of the "
+ + "SDK Tools installed. Make sure you update via the SDK Manager "
+ + "first. (Could not find %1$s)", file.getPath()));
+ return false;
+ }
+
+ NewProjectCreator creator = new NewProjectCreator(mValues, getContainer());
+ if (!(creator.createAndroidProjects())) {
+ return false;
+ }
+
+ // Open the default Java Perspective
+ OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction();
+ action.run();
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java
new file mode 100644
index 000000000..74af651ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import static com.android.SdkConstants.ATTR_NAME;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.AndroidManifestParser;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Activity;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.io.FolderWrapper;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+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.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** An Android project to be imported */
+class ImportedProject {
+ private final File mLocation;
+ private String mActivityName;
+ private ManifestData mManifest;
+ private String mProjectName;
+ private String mRelativePath;
+
+ ImportedProject(File location, String relativePath) {
+ super();
+ mLocation = location;
+ mRelativePath = relativePath;
+ }
+
+ File getLocation() {
+ return mLocation;
+ }
+
+ String getRelativePath() {
+ return mRelativePath;
+ }
+
+ @Nullable
+ ManifestData getManifest() {
+ if (mManifest == null) {
+ try {
+ mManifest = AndroidManifestParser.parse(new FolderWrapper(mLocation));
+ } catch (SAXException e) {
+ // Some sort of error in the manifest file: report to the user in a better way?
+ AdtPlugin.log(e, null);
+ return null;
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ return mManifest;
+ }
+
+ @Nullable
+ public String getActivityName() {
+ if (mActivityName == null) {
+ // Compute the project name and the package name from the manifest
+ ManifestData manifest = getManifest();
+ if (manifest != null) {
+ if (manifest.getLauncherActivity() != null) {
+ mActivityName = manifest.getLauncherActivity().getName();
+ }
+ if (mActivityName == null || mActivityName.isEmpty()) {
+ Activity[] activities = manifest.getActivities();
+ for (Activity activity : activities) {
+ mActivityName = activity.getName();
+ if (mActivityName != null && !mActivityName.isEmpty()) {
+ break;
+ }
+ }
+ }
+ if (mActivityName != null) {
+ int index = mActivityName.lastIndexOf('.');
+ mActivityName = mActivityName.substring(index + 1);
+ }
+ }
+ }
+
+ return mActivityName;
+ }
+
+ @NonNull
+ public String getProjectName() {
+ if (mProjectName == null) {
+ // Are we importing an Eclipse project? If so just use the existing project name
+ mProjectName = findEclipseProjectName();
+ if (mProjectName != null) {
+ return mProjectName;
+ }
+
+ String activityName = getActivityName();
+ if (activityName == null || activityName.isEmpty()) {
+ // I could also look at the build files, say build.xml from ant, and
+ // try to glean the project name from there
+ mProjectName = mLocation.getName();
+ } else {
+ // Try to derive it from the activity name:
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IStatus nameStatus = workspace.validateName(activityName, IResource.PROJECT);
+ if (nameStatus.isOK()) {
+ mProjectName = activityName;
+ } else {
+ // Try to derive it by escaping characters
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = activityName.length(); i < n; i++) {
+ char c = activityName.charAt(i);
+ if (c != IPath.DEVICE_SEPARATOR && c != IPath.SEPARATOR && c != '\\') {
+ sb.append(c);
+ }
+ }
+ if (sb.length() == 0) {
+ mProjectName = mLocation.getName();
+ } else {
+ mProjectName = sb.toString();
+ }
+ }
+ }
+ }
+
+ return mProjectName;
+ }
+
+ @Nullable
+ private String findEclipseProjectName() {
+ File projectFile = new File(mLocation, ".project"); //$NON-NLS-1$
+ if (projectFile.exists()) {
+ String xml;
+ try {
+ xml = Files.toString(projectFile, Charsets.UTF_8);
+ Document doc = DomUtilities.parseDocument(xml, false);
+ if (doc != null) {
+ NodeList names = doc.getElementsByTagName(ATTR_NAME);
+ if (names.getLength() >= 1) {
+ Node nameElement = names.item(0);
+ String name = nameElement.getTextContent().trim();
+ if (!name.isEmpty()) {
+ return name;
+ }
+ }
+ }
+ } catch (IOException e) {
+ // pass: don't attempt to read project name; must be some sort of unrelated
+ // file with the same name, perhaps from a different editor or IDE
+ }
+ }
+
+ return null;
+ }
+
+ public void setProjectName(@NonNull String newName) {
+ mProjectName = newName;
+ }
+
+ public IAndroidTarget getTarget() {
+ // Pick a target:
+ // First try to find the one requested by project.properties
+ IAndroidTarget[] targets = Sdk.getCurrent().getTargets();
+ ProjectProperties properties = ProjectProperties.load(mLocation.getPath(),
+ PropertyType.PROJECT);
+ if (properties != null) {
+ String targetProperty = properties.getProperty(ProjectProperties.PROPERTY_TARGET);
+ if (targetProperty != null) {
+ Matcher m = Pattern.compile("android-(.+)").matcher( //$NON-NLS-1$
+ targetProperty.trim());
+ if (m.matches()) {
+ String targetName = m.group(1);
+ int targetLevel;
+ try {
+ targetLevel = Integer.parseInt(targetName);
+ } catch (NumberFormatException nufe) {
+ // pass
+ targetLevel = -1;
+ }
+ for (IAndroidTarget t : targets) {
+ AndroidVersion version = t.getVersion();
+ if (version.isPreview() && targetName.equals(version.getCodename())) {
+ return t;
+ } else if (targetLevel == version.getApiLevel()) {
+ return t;
+ }
+ }
+ if (targetLevel > 0) {
+ // If not found, pick the closest one that is higher than the
+ // api level
+ IAndroidTarget target = targets[targets.length - 1];
+ int targetDelta = target.getVersion().getApiLevel() - targetLevel;
+ for (IAndroidTarget t : targets) {
+ int newDelta = t.getVersion().getApiLevel() - targetLevel;
+ if (newDelta >= 0 && newDelta < targetDelta) {
+ targetDelta = newDelta;
+ target = t;
+ }
+ }
+
+ return target;
+ }
+ }
+ }
+ }
+
+ // If not found, pick the closest one to the one requested by the
+ // project (in project.properties) that is still >= the minSdk version
+ IAndroidTarget target = targets[targets.length - 1];
+ ManifestData manifest = getManifest();
+ if (manifest != null) {
+ int minSdkLevel = manifest.getMinSdkVersion();
+ int targetDelta = target.getVersion().getApiLevel() - minSdkLevel;
+ for (IAndroidTarget t : targets) {
+ int newDelta = t.getVersion().getApiLevel() - minSdkLevel;
+ if (newDelta >= 0 && newDelta < targetDelta) {
+ targetDelta = newDelta;
+ target = t;
+ }
+ }
+ }
+
+ return target;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java
new file mode 100644
index 000000000..d168c7503
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java
@@ -0,0 +1,1520 @@
+/*
+ * 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import static com.android.SdkConstants.FN_PROJECT_PROPERTIES;
+import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_LIBRARY;
+
+import static org.eclipse.core.resources.IResource.DEPTH_ZERO;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.res2.ValueXmlHelper;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.AndroidNature;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+import com.android.io.StreamException;
+import com.android.resources.Density;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
+
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileInfo;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.filesystem.IFileSystem;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceStatus;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.jdt.core.IAccessRule;
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableContext;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IWorkingSet;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.WorkspaceModifyOperation;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * The actual project creator invoked from the New Project Wizard
+ * <p/>
+ * 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.
+ */
+public class NewProjectCreator {
+
+ private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$
+ private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$
+ private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$
+ private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$
+ private static final String PARAM_IMPORT_RESOURCE_CLASS = "IMPORT_RESOURCE_CLASS"; //$NON-NLS-1$
+ private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$
+ private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$
+ private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$
+ private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$
+ private static final String PARAM_SAMPLE_LOCATION = "SAMPLE_LOCATION"; //$NON-NLS-1$
+ private static final String PARAM_SOURCE = "SOURCE"; //$NON-NLS-1$
+ private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$
+ private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$
+ private static final String PARAM_IS_LIBRARY = "IS_LIBRARY"; //$NON-NLS-1$
+ private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$
+ // Warning: The expanded string PARAM_TEST_TARGET_PACKAGE must not contain the
+ // string "PACKAGE" since it collides with the replacement of PARAM_PACKAGE.
+ private static final String PARAM_TEST_TARGET_PACKAGE = "TEST_TARGET_PCKG"; //$NON-NLS-1$
+ private static final String PARAM_TARGET_SELF = "TARGET_SELF"; //$NON-NLS-1$
+ private static final String PARAM_TARGET_MAIN = "TARGET_MAIN"; //$NON-NLS-1$
+ private static final String PARAM_TARGET_EXISTING = "TARGET_EXISTING"; //$NON-NLS-1$
+ private static final String PARAM_REFERENCE_PROJECT = "REFERENCE_PROJECT"; //$NON-NLS-1$
+
+ private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$
+ private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$
+ private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$
+ private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$
+ private static final String PH_TEST_USES_LIBRARY = "TEST-USES-LIBRARY"; //$NON-NLS-1$
+ private static final String PH_TEST_INSTRUMENTATION = "TEST-INSTRUMENTATION"; //$NON-NLS-1$
+
+ private static final String BIN_DIRECTORY =
+ SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP;
+ private static final String BIN_CLASSES_DIRECTORY =
+ SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP +
+ SdkConstants.FD_CLASSES_OUTPUT + AdtConstants.WS_SEP;
+ private static final String RES_DIRECTORY =
+ SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP;
+ private static final String ASSETS_DIRECTORY =
+ SdkConstants.FD_ASSETS + AdtConstants.WS_SEP;
+ private static final String DRAWABLE_DIRECTORY =
+ SdkConstants.FD_RES_DRAWABLE + AdtConstants.WS_SEP;
+ private static final String DRAWABLE_XHDPI_DIRECTORY =
+ SdkConstants.FD_RES_DRAWABLE + '-' + Density.XHIGH.getResourceValue() +
+ AdtConstants.WS_SEP;
+ private static final String DRAWABLE_HDPI_DIRECTORY =
+ SdkConstants.FD_RES_DRAWABLE + '-' + Density.HIGH.getResourceValue() +
+ AdtConstants.WS_SEP;
+ private static final String DRAWABLE_MDPI_DIRECTORY =
+ SdkConstants.FD_RES_DRAWABLE + '-' + Density.MEDIUM.getResourceValue() +
+ AdtConstants.WS_SEP;
+ private static final String DRAWABLE_LDPI_DIRECTORY =
+ SdkConstants.FD_RES_DRAWABLE + '-' + Density.LOW.getResourceValue() +
+ AdtConstants.WS_SEP;
+ private static final String LAYOUT_DIRECTORY =
+ SdkConstants.FD_RES_LAYOUT + AdtConstants.WS_SEP;
+ private static final String VALUES_DIRECTORY =
+ SdkConstants.FD_RES_VALUES + AdtConstants.WS_SEP;
+ private static final String GEN_SRC_DIRECTORY =
+ SdkConstants.FD_GEN_SOURCES + AdtConstants.WS_SEP;
+
+ private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$
+ private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY
+ + "AndroidManifest.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY
+ + "activity.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY
+ + "uses-sdk.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY
+ + "launcher_intent_filter.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_TEST_USES_LIBRARY = TEMPLATES_DIRECTORY
+ + "test_uses-library.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_TEST_INSTRUMENTATION = TEMPLATES_DIRECTORY
+ + "test_instrumentation.template"; //$NON-NLS-1$
+
+
+
+ private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY
+ + "strings.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY
+ + "string.template"; //$NON-NLS-1$
+ private static final String PROJECT_ICON = "ic_launcher.png"; //$NON-NLS-1$
+ private static final String ICON_XHDPI = "ic_launcher_xhdpi.png"; //$NON-NLS-1$
+ private static final String ICON_HDPI = "ic_launcher_hdpi.png"; //$NON-NLS-1$
+ private static final String ICON_MDPI = "ic_launcher_mdpi.png"; //$NON-NLS-1$
+ private static final String ICON_LDPI = "ic_launcher_ldpi.png"; //$NON-NLS-1$
+
+ private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$
+
+ private static final String STRING_RSRC_PREFIX = SdkConstants.STRING_PREFIX;
+ private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$
+ private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$
+
+ private static final String[] DEFAULT_DIRECTORIES = new String[] {
+ BIN_DIRECTORY, BIN_CLASSES_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY };
+ private static final String[] RES_DIRECTORIES = new String[] {
+ DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY };
+ private static final String[] RES_DENSITY_ENABLED_DIRECTORIES = new String[] {
+ DRAWABLE_XHDPI_DIRECTORY,
+ DRAWABLE_HDPI_DIRECTORY, DRAWABLE_MDPI_DIRECTORY, DRAWABLE_LDPI_DIRECTORY,
+ LAYOUT_DIRECTORY, VALUES_DIRECTORY };
+
+ private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$
+ private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$
+ private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$
+
+ private final NewProjectWizardState mValues;
+ private final IRunnableContext mRunnableContext;
+
+ /**
+ * Creates a new {@linkplain NewProjectCreator}
+ * @param values the wizard state with initial project parameters
+ * @param runnableContext the context to run project creation in
+ */
+ public NewProjectCreator(NewProjectWizardState values, IRunnableContext runnableContext) {
+ mValues = values;
+ mRunnableContext = runnableContext;
+ }
+
+ /**
+ * Before actually creating the project for a new project (as opposed to using an
+ * existing project), we check if the target location is a directory that either does
+ * not exist or is empty.
+ *
+ * If it's not empty, ask the user for confirmation.
+ *
+ * @param destination The destination folder where the new project is to be created.
+ * @return True if the destination doesn't exist yet or is an empty directory or is
+ * accepted by the user.
+ */
+ private boolean validateNewProjectLocationIsEmpty(IPath destination) {
+ File f = new File(destination.toOSString());
+ if (f.isDirectory() && f.list().length > 0) {
+ return AdtPlugin.displayPrompt("New Android Project",
+ "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?");
+ }
+ return true;
+ }
+
+ /**
+ * Structure that describes all the information needed to create a project.
+ * This is collected from the pages by {@link NewProjectCreator#createAndroidProjects()}
+ * and then used by
+ * {@link NewProjectCreator#createProjectAsync(IProgressMonitor, ProjectInfo, ProjectInfo)}.
+ */
+ private static class ProjectInfo {
+ private final IProject mProject;
+ private final IProjectDescription mDescription;
+ private final Map<String, Object> mParameters;
+ private final HashMap<String, String> mDictionary;
+
+ public ProjectInfo(IProject project,
+ IProjectDescription description,
+ Map<String, Object> parameters,
+ HashMap<String, String> dictionary) {
+ mProject = project;
+ mDescription = description;
+ mParameters = parameters;
+ mDictionary = dictionary;
+ }
+
+ public IProject getProject() {
+ return mProject;
+ }
+
+ public IProjectDescription getDescription() {
+ return mDescription;
+ }
+
+ public Map<String, Object> getParameters() {
+ return mParameters;
+ }
+
+ public HashMap<String, String> getDictionary() {
+ return mDictionary;
+ }
+ }
+
+ /**
+ * Creates the android project.
+ * @return True if the project could be created.
+ */
+ public boolean createAndroidProjects() {
+ if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) {
+ return importProjects();
+ }
+
+ final ProjectInfo mainData = collectMainPageInfo();
+ final ProjectInfo testData = collectTestPageInfo();
+
+ // Create a monitored operation to create the actual project
+ WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
+ @Override
+ protected void execute(IProgressMonitor monitor) throws InvocationTargetException {
+ createProjectAsync(monitor, mainData, testData, null, true);
+ }
+ };
+
+ // Run the operation in a different thread
+ runAsyncOperation(op);
+ return true;
+ }
+
+ /**
+ * Creates the a plain Java project without typical android directories or an Android Nature.
+ * This is intended for use by unit tests and not as a general-purpose Java project creator.
+ * @return True if the project could be created.
+ */
+ @VisibleForTesting
+ public boolean createJavaProjects() {
+ if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) {
+ return importProjects();
+ }
+
+ final ProjectInfo mainData = collectMainPageInfo();
+ final ProjectInfo testData = collectTestPageInfo();
+
+ // Create a monitored operation to create the actual project
+ WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
+ @Override
+ protected void execute(IProgressMonitor monitor) throws InvocationTargetException {
+ createProjectAsync(monitor, mainData, testData, null, false);
+ }
+ };
+
+ // Run the operation in a different thread
+ runAsyncOperation(op);
+ return true;
+ }
+
+ /**
+ * Imports a list of projects
+ */
+ private boolean importProjects() {
+ assert mValues.importProjects != null && !mValues.importProjects.isEmpty();
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+
+ final List<ProjectInfo> projectData = new ArrayList<ProjectInfo>();
+ for (ImportedProject p : mValues.importProjects) {
+
+ // Compute the project name and the package name from the manifest
+ ManifestData manifest = p.getManifest();
+ if (manifest == null) {
+ continue;
+ }
+ String packageName = manifest.getPackage();
+ String projectName = p.getProjectName();
+ String minSdk = manifest.getMinSdkVersionString();
+
+ final IProject project = workspace.getRoot().getProject(projectName);
+ final IProjectDescription description =
+ workspace.newProjectDescription(project.getName());
+
+ final Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put(PARAM_PROJECT, projectName);
+ parameters.put(PARAM_PACKAGE, packageName);
+ parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder());
+ parameters.put(PARAM_IS_NEW_PROJECT, Boolean.FALSE);
+ parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES);
+
+ parameters.put(PARAM_SDK_TARGET, p.getTarget());
+
+ // TODO: Find out if these end up getting used in the import-path through the code!
+ parameters.put(PARAM_MIN_SDK_VERSION, minSdk);
+ parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME);
+ final HashMap<String, String> dictionary = new HashMap<String, String>();
+ dictionary.put(STRING_APP_NAME, mValues.applicationName);
+
+ if (mValues.copyIntoWorkspace) {
+ parameters.put(PARAM_SOURCE, p.getLocation());
+
+ // TODO: Make sure it isn't *already* in the workspace!
+ //IPath defaultLocation = Platform.getLocation();
+ //if ((!mValues.useDefaultLocation || mValues.useExisting)
+ // && !defaultLocation.isPrefixOf(path)) {
+ //IPath workspaceLocation = Platform.getLocation().append(projectName);
+ //description.setLocation(workspaceLocation);
+ // DON'T SET THE LOCATION: It's IMPLIED and in fact it will generate
+ // an error if you set it!
+ } else {
+ // Create in place
+ description.setLocation(new Path(p.getLocation().getPath()));
+ }
+
+ projectData.add(new ProjectInfo(project, description, parameters, dictionary));
+ }
+
+ // Create a monitored operation to create the actual project
+ WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
+ @Override
+ protected void execute(IProgressMonitor monitor) throws InvocationTargetException {
+ createProjectAsync(monitor, null, null, projectData, true);
+ }
+ };
+
+ // Run the operation in a different thread
+ runAsyncOperation(op);
+ return true;
+ }
+
+ /**
+ * Collects all the parameters needed to create the main project.
+ * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be
+ * created because parameters are incorrect or should not be created because there
+ * is no main page.
+ */
+ private ProjectInfo collectMainPageInfo() {
+ if (mValues.mode == Mode.TEST) {
+ return null;
+ }
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ final IProject project = workspace.getRoot().getProject(mValues.projectName);
+ final IProjectDescription description = workspace.newProjectDescription(project.getName());
+
+ final Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put(PARAM_PROJECT, mValues.projectName);
+ parameters.put(PARAM_PACKAGE, mValues.packageName);
+ parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME);
+ parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder());
+ parameters.put(PARAM_IS_NEW_PROJECT, mValues.mode == Mode.ANY && !mValues.useExisting);
+ parameters.put(PARAM_SAMPLE_LOCATION, mValues.chosenSample);
+ parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder);
+ parameters.put(PARAM_SDK_TARGET, mValues.target);
+ parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk);
+
+ if (mValues.createActivity) {
+ parameters.put(PARAM_ACTIVITY, mValues.activityName);
+ }
+
+ // create a dictionary of string that will contain name+content.
+ // we'll put all the strings into values/strings.xml
+ final HashMap<String, String> dictionary = new HashMap<String, String>();
+ dictionary.put(STRING_APP_NAME, mValues.applicationName);
+
+ IPath path = new Path(mValues.projectLocation.getPath());
+ IPath defaultLocation = Platform.getLocation();
+ if ((!mValues.useDefaultLocation || mValues.useExisting)
+ && !defaultLocation.isPrefixOf(path)) {
+ description.setLocation(path);
+ }
+
+ if (mValues.mode == Mode.ANY && !mValues.useExisting && !mValues.useDefaultLocation &&
+ !validateNewProjectLocationIsEmpty(path)) {
+ return null;
+ }
+
+ return new ProjectInfo(project, description, parameters, dictionary);
+ }
+
+ /**
+ * Collects all the parameters needed to create the test project.
+ *
+ * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be
+ * created because parameters are incorrect or should not be created because there
+ * is no test page.
+ */
+ private ProjectInfo collectTestPageInfo() {
+ if (mValues.mode != Mode.TEST && !mValues.createPairProject) {
+ return null;
+ }
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ String projectName =
+ mValues.mode == Mode.TEST ? mValues.projectName : mValues.testProjectName;
+ final IProject project = workspace.getRoot().getProject(projectName);
+ final IProjectDescription description = workspace.newProjectDescription(project.getName());
+
+ final Map<String, Object> parameters = new HashMap<String, Object>();
+
+ String pkg =
+ mValues.mode == Mode.TEST ? mValues.packageName : mValues.testPackageName;
+
+ parameters.put(PARAM_PACKAGE, pkg);
+ parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME);
+ parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder());
+ parameters.put(PARAM_IS_NEW_PROJECT, !mValues.useExisting);
+ parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder);
+ parameters.put(PARAM_SDK_TARGET, mValues.target);
+ parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk);
+
+ // Test-specific parameters
+ String testedPkg = mValues.createPairProject
+ ? mValues.packageName : mValues.testTargetPackageName;
+ if (testedPkg == null) {
+ assert mValues.testingSelf;
+ testedPkg = pkg;
+ }
+
+ parameters.put(PARAM_TEST_TARGET_PACKAGE, testedPkg);
+
+ if (mValues.testingSelf) {
+ parameters.put(PARAM_TARGET_SELF, true);
+ } else {
+ parameters.put(PARAM_TARGET_EXISTING, true);
+ parameters.put(PARAM_REFERENCE_PROJECT, mValues.testedProject);
+ }
+
+ if (mValues.createPairProject) {
+ parameters.put(PARAM_TARGET_MAIN, true);
+ }
+
+ // create a dictionary of string that will contain name+content.
+ // we'll put all the strings into values/strings.xml
+ final HashMap<String, String> dictionary = new HashMap<String, String>();
+ dictionary.put(STRING_APP_NAME, mValues.testApplicationName);
+
+ // Use the same logic to determine test project location as in
+ // ApplicationInfoPage#validateTestProjectLocation
+ IPath path = new Path(mValues.projectLocation.getPath());
+ path = path.removeLastSegments(1).append(mValues.testProjectName);
+ IPath defaultLocation = Platform.getLocation();
+ if ((!mValues.useDefaultLocation || mValues.useExisting)
+ && !path.equals(defaultLocation)) {
+ description.setLocation(path);
+ }
+
+ if (!mValues.useExisting && !mValues.useDefaultLocation &&
+ !validateNewProjectLocationIsEmpty(path)) {
+ return null;
+ }
+
+ return new ProjectInfo(project, description, parameters, dictionary);
+ }
+
+ /**
+ * Runs the operation in a different thread and display generated
+ * exceptions.
+ *
+ * @param op The asynchronous operation to run.
+ */
+ private void runAsyncOperation(WorkspaceModifyOperation op) {
+ try {
+ mRunnableContext.run(true /* fork */, true /* cancelable */, op);
+ } catch (InvocationTargetException e) {
+
+ AdtPlugin.log(e, "New Project Wizard failed");
+
+ // The runnable threw an exception
+ Throwable t = e.getTargetException();
+ if (t instanceof CoreException) {
+ CoreException core = (CoreException) t;
+ if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) {
+ // The error indicates the file system is not case sensitive
+ // and there's a resource with a similar name.
+ MessageDialog.openError(AdtPlugin.getShell(),
+ "Error", "Error: Case Variant Exists");
+ } else {
+ ErrorDialog.openError(AdtPlugin.getShell(),
+ "Error", core.getMessage(), core.getStatus());
+ }
+ } else {
+ // Some other kind of exception
+ String msg = t.getMessage();
+ Throwable t1 = t;
+ while (msg == null && t1.getCause() != null) {
+ msg = t1.getMessage();
+ t1 = t1.getCause();
+ }
+ if (msg == null) {
+ msg = t.toString();
+ }
+ MessageDialog.openError(AdtPlugin.getShell(), "Error", msg);
+ }
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Creates the actual project(s). This is run asynchronously in a different thread.
+ *
+ * @param monitor An existing monitor.
+ * @param mainData Data for main project. Can be null.
+ * @param isAndroidProject true if the project is to be set up as a full Android project; false
+ * for a plain Java project.
+ * @throws InvocationTargetException to wrap any unmanaged exception and
+ * return it to the calling thread. The method can fail if it fails
+ * to create or modify the project or if it is canceled by the user.
+ */
+ private void createProjectAsync(IProgressMonitor monitor,
+ ProjectInfo mainData,
+ ProjectInfo testData,
+ List<ProjectInfo> importData,
+ boolean isAndroidProject)
+ throws InvocationTargetException {
+ monitor.beginTask("Create Android Project", 100);
+ try {
+ IProject mainProject = null;
+
+ if (mainData != null) {
+ mainProject = createEclipseProject(
+ new SubProgressMonitor(monitor, 50),
+ mainData.getProject(),
+ mainData.getDescription(),
+ mainData.getParameters(),
+ mainData.getDictionary(),
+ null,
+ isAndroidProject);
+
+ if (mainProject != null) {
+ final IJavaProject javaProject = JavaCore.create(mainProject);
+ Display.getDefault().syncExec(new WorksetAdder(javaProject,
+ mValues.workingSets));
+ }
+ }
+
+ if (testData != null) {
+ Map<String, Object> parameters = testData.getParameters();
+ if (parameters.containsKey(PARAM_TARGET_MAIN) && mainProject != null) {
+ parameters.put(PARAM_REFERENCE_PROJECT, mainProject);
+ }
+
+ IProject testProject = createEclipseProject(
+ new SubProgressMonitor(monitor, 50),
+ testData.getProject(),
+ testData.getDescription(),
+ parameters,
+ testData.getDictionary(),
+ null,
+ isAndroidProject);
+ if (testProject != null) {
+ final IJavaProject javaProject = JavaCore.create(testProject);
+ Display.getDefault().syncExec(new WorksetAdder(javaProject,
+ mValues.workingSets));
+ }
+ }
+
+ if (importData != null) {
+ for (final ProjectInfo data : importData) {
+ ProjectPopulator projectPopulator = null;
+ if (mValues.copyIntoWorkspace) {
+ projectPopulator = new ProjectPopulator() {
+ @Override
+ public void populate(IProject project) {
+ // Copy
+ IFileSystem fileSystem = EFS.getLocalFileSystem();
+ File source = (File) data.getParameters().get(PARAM_SOURCE);
+ IFileStore sourceDir = new ReadWriteFileStore(
+ fileSystem.getStore(source.toURI()));
+ IFileStore destDir = new ReadWriteFileStore(
+ fileSystem.getStore(AdtUtils.getAbsolutePath(project)));
+ try {
+ sourceDir.copy(destDir, EFS.OVERWRITE, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ };
+ }
+ IProject project = createEclipseProject(
+ new SubProgressMonitor(monitor, 50),
+ data.getProject(),
+ data.getDescription(),
+ data.getParameters(),
+ data.getDictionary(),
+ projectPopulator,
+ isAndroidProject);
+ if (project != null) {
+ final IJavaProject javaProject = JavaCore.create(project);
+ Display.getDefault().syncExec(new WorksetAdder(javaProject,
+ mValues.workingSets));
+ ProjectHelper.enforcePreferredCompilerCompliance(javaProject);
+ }
+ }
+ }
+ } catch (CoreException e) {
+ throw new InvocationTargetException(e);
+ } catch (IOException e) {
+ throw new InvocationTargetException(e);
+ } catch (StreamException e) {
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ /** Handler which can write contents into a project */
+ public interface ProjectPopulator {
+ /**
+ * Add contents into the given project
+ *
+ * @param project the project to write into
+ * @throws InvocationTargetException if anything goes wrong
+ */
+ public void populate(IProject project) throws InvocationTargetException;
+ }
+
+ /**
+ * Creates the actual project, sets its nature and adds the required folders
+ * and files to it. This is run asynchronously in a different thread.
+ *
+ * @param monitor An existing monitor.
+ * @param project The project to create.
+ * @param description A description of the project.
+ * @param parameters Template parameters.
+ * @param dictionary String definition.
+ * @param isAndroidProject true if the project is to be set up as a full Android project; false
+ * for a plain Java project.
+ * @return The project newly created
+ * @throws StreamException
+ */
+ private IProject createEclipseProject(
+ @NonNull IProgressMonitor monitor,
+ @NonNull IProject project,
+ @NonNull IProjectDescription description,
+ @NonNull Map<String, Object> parameters,
+ @Nullable Map<String, String> dictionary,
+ @Nullable ProjectPopulator projectPopulator,
+ boolean isAndroidProject)
+ throws CoreException, IOException, StreamException {
+
+ // get the project target
+ IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET);
+ boolean legacy = isAndroidProject && target.getVersion().getApiLevel() < 4;
+
+ // Create project and open it
+ project.create(description, new SubProgressMonitor(monitor, 10));
+ if (monitor.isCanceled()) throw new OperationCanceledException();
+
+ project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10));
+
+ // Add the Java and android nature to the project
+ AndroidNature.setupProjectNatures(project, monitor, isAndroidProject);
+
+ // Create folders in the project if they don't already exist
+ addDefaultDirectories(project, AdtConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor);
+ String[] sourceFolders;
+ if (isAndroidProject) {
+ sourceFolders = new String[] {
+ (String) parameters.get(PARAM_SRC_FOLDER),
+ GEN_SRC_DIRECTORY
+ };
+ } else {
+ sourceFolders = new String[] {
+ (String) parameters.get(PARAM_SRC_FOLDER)
+ };
+ }
+ addDefaultDirectories(project, AdtConstants.WS_ROOT, sourceFolders, monitor);
+
+ // Create the resource folders in the project if they don't already exist.
+ if (legacy) {
+ addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor);
+ } else {
+ addDefaultDirectories(project, RES_DIRECTORY, RES_DENSITY_ENABLED_DIRECTORIES, monitor);
+ }
+
+ if (projectPopulator != null) {
+ try {
+ projectPopulator.populate(project);
+ } catch (InvocationTargetException ite) {
+ AdtPlugin.log(ite, null);
+ }
+ }
+
+ // Setup class path: mark folders as source folders
+ IJavaProject javaProject = JavaCore.create(project);
+ setupSourceFolders(javaProject, sourceFolders, monitor);
+
+ if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) {
+ // Create files in the project if they don't already exist
+ addManifest(project, parameters, dictionary, monitor);
+
+ // add the default app icon
+ addIcon(project, legacy, monitor);
+
+ // Create the default package components
+ addSampleCode(project, sourceFolders[0], parameters, dictionary, monitor);
+
+ // add the string definition file if needed
+ if (dictionary != null && dictionary.size() > 0) {
+ addStringDictionaryFile(project, dictionary, monitor);
+ }
+
+ // add the default proguard config
+ File libFolder = new File((String) parameters.get(PARAM_SDK_TOOLS_DIR),
+ SdkConstants.FD_LIB);
+ addLocalFile(project,
+ new File(libFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE),
+ // Write ProGuard config files with the extension .pro which
+ // is what is used in the ProGuard documentation and samples
+ SdkConstants.FN_PROJECT_PROGUARD_FILE,
+ monitor);
+
+ // Set output location
+ javaProject.setOutputLocation(project.getFolder(BIN_CLASSES_DIRECTORY).getFullPath(),
+ monitor);
+ }
+
+ File sampleDir = (File) parameters.get(PARAM_SAMPLE_LOCATION);
+ if (sampleDir != null) {
+ // Copy project
+ copySampleCode(project, sampleDir, parameters, dictionary, monitor);
+ }
+
+ // Create the reference to the target project
+ if (parameters.containsKey(PARAM_REFERENCE_PROJECT)) {
+ IProject refProject = (IProject) parameters.get(PARAM_REFERENCE_PROJECT);
+ if (refProject != null) {
+ IProjectDescription desc = project.getDescription();
+
+ // Add out reference to the existing project reference.
+ // We just created a project with no references so we don't need to expand
+ // the currently-empty current list.
+ desc.setReferencedProjects(new IProject[] { refProject });
+
+ project.setDescription(desc, IResource.KEEP_HISTORY,
+ new SubProgressMonitor(monitor, 10));
+
+ IClasspathEntry entry = JavaCore.newProjectEntry(
+ refProject.getFullPath(), //path
+ new IAccessRule[0], //accessRules
+ false, //combineAccessRules
+ new IClasspathAttribute[0], //extraAttributes
+ false //isExported
+
+ );
+ ProjectHelper.addEntryToClasspath(javaProject, entry);
+ }
+ }
+
+ if (isAndroidProject) {
+ Sdk.getCurrent().initProject(project, target);
+ }
+
+ // Fix the project to make sure all properties are as expected.
+ // Necessary for existing projects and good for new ones to.
+ ProjectHelper.fixProject(project);
+
+ Boolean isLibraryProject = (Boolean) parameters.get(PARAM_IS_LIBRARY);
+ if (isLibraryProject != null && isLibraryProject.booleanValue()
+ && Sdk.getCurrent() != null && project.isOpen()) {
+ ProjectState state = Sdk.getProjectState(project);
+ if (state != null) {
+ // make a working copy of the properties
+ ProjectPropertiesWorkingCopy properties =
+ state.getProperties().makeWorkingCopy();
+
+ properties.setProperty(PROPERTY_LIBRARY, Boolean.TRUE.toString());
+ try {
+ properties.save();
+ IResource projectProp = project.findMember(FN_PROJECT_PROPERTIES);
+ if (projectProp != null) {
+ projectProp.refreshLocal(DEPTH_ZERO, new NullProgressMonitor());
+ }
+ } catch (Exception e) {
+ String msg = String.format(
+ "Failed to save %1$s for project %2$s",
+ SdkConstants.FN_PROJECT_PROPERTIES, project.getName());
+ AdtPlugin.log(e, msg);
+ }
+ }
+ }
+
+ return project;
+ }
+
+ /**
+ * Creates a new project
+ *
+ * @param monitor An existing monitor.
+ * @param project The project to create.
+ * @param target the build target to associate with the project
+ * @param projectPopulator a handler for writing the template contents
+ * @param isLibrary whether this project should be marked as a library project
+ * @param projectLocation the location to write the project into
+ * @param workingSets Eclipse working sets, if any, to add the project to
+ * @throws CoreException if anything goes wrong
+ */
+ public static void create(
+ @NonNull IProgressMonitor monitor,
+ @NonNull final IProject project,
+ @NonNull IAndroidTarget target,
+ @Nullable final ProjectPopulator projectPopulator,
+ boolean isLibrary,
+ @NonNull String projectLocation,
+ @NonNull final IWorkingSet[] workingSets)
+ throws CoreException {
+ final NewProjectCreator creator = new NewProjectCreator(null, null);
+
+ final Map<String, String> dictionary = null;
+ final Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put(PARAM_SDK_TARGET, target);
+ parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES);
+ parameters.put(PARAM_IS_NEW_PROJECT, false);
+ parameters.put(PARAM_SAMPLE_LOCATION, null);
+ parameters.put(PARAM_IS_LIBRARY, isLibrary);
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ final IProjectDescription description = workspace.newProjectDescription(project.getName());
+
+ if (projectLocation != null) {
+ IPath path = new Path(projectLocation);
+ IPath parent = new Path(path.toFile().getParent());
+ IPath workspaceLocation = Platform.getLocation();
+ if (!workspaceLocation.equals(parent)) {
+ description.setLocation(path);
+ }
+ }
+
+ IWorkspaceRunnable workspaceRunnable = new IWorkspaceRunnable() {
+ @Override
+ public void run(IProgressMonitor submonitor) throws CoreException {
+ try {
+ creator.createEclipseProject(submonitor, project, description, parameters,
+ dictionary, projectPopulator, true);
+ } catch (IOException e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Unexpected error while creating project", e));
+ } catch (StreamException e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Unexpected error while creating project", e));
+ }
+ if (workingSets != null && workingSets.length > 0) {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ Display.getDefault().syncExec(new WorksetAdder(javaProject,
+ workingSets));
+ }
+ }
+ }
+ };
+
+ ResourcesPlugin.getWorkspace().run(workspaceRunnable, monitor);
+ }
+
+ /**
+ * Adds default directories to the project.
+ *
+ * @param project The Java Project to update.
+ * @param parentFolder The path of the parent folder. Must end with a
+ * separator.
+ * @param folders Folders to be added.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to create the directories in
+ * the project.
+ */
+ private void addDefaultDirectories(IProject project, String parentFolder,
+ String[] folders, IProgressMonitor monitor) throws CoreException {
+ for (String name : folders) {
+ if (name.length() > 0) {
+ IFolder folder = project.getFolder(parentFolder + name);
+ if (!folder.exists()) {
+ folder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the manifest to the project.
+ *
+ * @param project The Java Project to update.
+ * @param parameters Template Parameters.
+ * @param dictionary String List to be added to a string definition
+ * file. This map will be filled by this method.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addManifest(IProject project, Map<String, Object> parameters,
+ Map<String, String> dictionary, IProgressMonitor monitor)
+ throws CoreException, IOException {
+
+ // get IFile to the manifest and check if it's not already there.
+ IFile file = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
+ if (!file.exists()) {
+
+ // Read manifest template
+ String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST);
+
+ // Replace all keyword parameters
+ manifestTemplate = replaceParameters(manifestTemplate, parameters);
+
+ if (manifestTemplate == null) {
+ // Inform the user there will be not manifest.
+ AdtPlugin.logAndPrintError(null, "Create Project" /*TAG*/,
+ "Failed to generate the Android manifest. Missing template %s",
+ TEMPLATE_MANIFEST);
+ // Abort now, there's no need to continue
+ return;
+ }
+
+ if (parameters.containsKey(PARAM_ACTIVITY)) {
+ // now get the activity template
+ String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES);
+
+ // If the activity name doesn't contain any dot, it's in the form
+ // "ClassName" and we need to expand it to ".ClassName" in the XML.
+ String name = (String) parameters.get(PARAM_ACTIVITY);
+ if (name.indexOf('.') == -1) {
+ // Duplicate the parameters map to avoid changing the caller
+ parameters = new HashMap<String, Object>(parameters);
+ parameters.put(PARAM_ACTIVITY, "." + name); //$NON-NLS-1$
+ }
+
+ // Replace all keyword parameters to make main activity.
+ String activities = replaceParameters(activityTemplate, parameters);
+
+ // set the intent.
+ String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER);
+
+ if (activities != null) {
+ if (intent != null) {
+ // set the intent to the main activity
+ activities = activities.replaceAll(PH_INTENT_FILTERS, intent);
+ }
+
+ // set the activity(ies) in the manifest
+ manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities);
+ }
+ } else {
+ // remove the activity(ies) from the manifest
+ manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, ""); //$NON-NLS-1$
+ }
+
+ // Handle the case of the test projects
+ if (parameters.containsKey(PARAM_TEST_TARGET_PACKAGE)) {
+ // Set the uses-library needed by the test project
+ String usesLibrary = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_USES_LIBRARY);
+ if (usesLibrary != null) {
+ manifestTemplate = manifestTemplate.replaceAll(
+ PH_TEST_USES_LIBRARY, usesLibrary);
+ }
+
+ // Set the instrumentation element needed by the test project
+ String instru = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_INSTRUMENTATION);
+ if (instru != null) {
+ manifestTemplate = manifestTemplate.replaceAll(
+ PH_TEST_INSTRUMENTATION, instru);
+ }
+
+ // Replace PARAM_TEST_TARGET_PACKAGE itself now
+ manifestTemplate = replaceParameters(manifestTemplate, parameters);
+
+ } else {
+ // remove the unused entries
+ manifestTemplate = manifestTemplate.replaceAll(PH_TEST_USES_LIBRARY, ""); //$NON-NLS-1$
+ manifestTemplate = manifestTemplate.replaceAll(PH_TEST_INSTRUMENTATION, ""); //$NON-NLS-1$
+ }
+
+ String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION);
+ if (minSdkVersion != null && minSdkVersion.length() > 0) {
+ String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK);
+ if (usesSdkTemplate != null) {
+ String usesSdk = replaceParameters(usesSdkTemplate, parameters);
+ manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk);
+ }
+ } else {
+ manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, "");
+ }
+
+ // Reformat the file according to the user's formatting settings
+ manifestTemplate = reformat(XmlFormatStyle.MANIFEST, manifestTemplate);
+
+ // Save in the project as UTF-8
+ InputStream stream = new ByteArrayInputStream(
+ manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$
+ file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /**
+ * Adds the string resource file.
+ *
+ * @param project The Java Project to update.
+ * @param strings The list of strings to be added to the string file.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addStringDictionaryFile(IProject project,
+ Map<String, String> strings, IProgressMonitor monitor)
+ throws CoreException, IOException {
+
+ // create the IFile object and check if the file doesn't already exist.
+ IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + VALUES_DIRECTORY + AdtConstants.WS_SEP + STRINGS_FILE);
+ if (!file.exists()) {
+ // get the Strings.xml template
+ String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS);
+
+ // get the template for one string
+ String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING);
+
+ // get all the string names
+ Set<String> stringNames = strings.keySet();
+
+ // loop on it and create the string definitions
+ StringBuilder stringNodes = new StringBuilder();
+ for (String key : stringNames) {
+ // get the value from the key
+ String value = strings.get(key);
+
+ // Escape values if necessary
+ value = ValueXmlHelper.escapeResourceString(value);
+
+ // place them in the template
+ String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key);
+ stringDef = stringDef.replace(PARAM_STRING_CONTENT, value);
+
+ // append to the other string
+ if (stringNodes.length() > 0) {
+ stringNodes.append('\n');
+ }
+ stringNodes.append(stringDef);
+ }
+
+ // put the string nodes in the Strings.xml template
+ stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS,
+ stringNodes.toString());
+
+ // reformat the file according to the user's formatting settings
+ stringDefinitionTemplate = reformat(XmlFormatStyle.RESOURCE, stringDefinitionTemplate);
+
+ // write the file as UTF-8
+ InputStream stream = new ByteArrayInputStream(
+ stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$
+ file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /** Reformats the given contents with the current formatting settings */
+ private String reformat(XmlFormatStyle style, String contents) {
+ if (AdtPrefs.getPrefs().getUseCustomXmlFormatter()) {
+ EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
+ return EclipseXmlPrettyPrinter.prettyPrint(contents, formatPrefs, style,
+ null /*lineSeparator*/);
+ } else {
+ return contents;
+ }
+ }
+
+ /**
+ * Adds default application icon to the project.
+ *
+ * @param project The Java Project to update.
+ * @param legacy whether we're running in legacy mode (no density support)
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ */
+ private void addIcon(IProject project, boolean legacy, IProgressMonitor monitor)
+ throws CoreException {
+ if (legacy) { // density support
+ // do medium density icon only, in the default drawable folder.
+ IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + DRAWABLE_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON);
+ if (!file.exists()) {
+ addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor);
+ }
+ } else {
+ // do all 4 icons.
+ IFile file;
+
+ // extra high density
+ file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + DRAWABLE_XHDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON);
+ if (!file.exists()) {
+ addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_XHDPI), monitor);
+ }
+
+ // high density
+ file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + DRAWABLE_HDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON);
+ if (!file.exists()) {
+ addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_HDPI), monitor);
+ }
+
+ // medium density
+ file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + DRAWABLE_MDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON);
+ if (!file.exists()) {
+ addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor);
+ }
+
+ // low density
+ file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP
+ + DRAWABLE_LDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON);
+ if (!file.exists()) {
+ addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_LDPI), monitor);
+ }
+ }
+ }
+
+ /**
+ * Creates a file from a data source.
+ * @param dest the file to write
+ * @param source the content of the file.
+ * @param monitor the progress monitor
+ * @throws CoreException
+ */
+ private void addFile(IFile dest, byte[] source, IProgressMonitor monitor) throws CoreException {
+ if (source != null) {
+ // Save in the project
+ InputStream stream = new ByteArrayInputStream(source);
+ dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /**
+ * Creates the package folder and copies the sample code in the project.
+ *
+ * @param project The Java Project to update.
+ * @param parameters Template Parameters.
+ * @param dictionary String List to be added to a string definition
+ * file. This map will be filled by this method.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addSampleCode(IProject project, String sourceFolder,
+ Map<String, Object> parameters, Map<String, String> dictionary,
+ IProgressMonitor monitor) throws CoreException, IOException {
+ // create the java package directories.
+ IFolder pkgFolder = project.getFolder(sourceFolder);
+ String packageName = (String) parameters.get(PARAM_PACKAGE);
+
+ // The PARAM_ACTIVITY key will be absent if no activity should be created,
+ // in which case activityName will be null.
+ String activityName = (String) parameters.get(PARAM_ACTIVITY);
+
+ Map<String, Object> java_activity_parameters = new HashMap<String, Object>(parameters);
+ java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, ""); //$NON-NLS-1$
+
+ if (activityName != null) {
+
+ String resourcePackageClass = null;
+
+ // An activity name can be of the form ".package.Class", ".Class" or FQDN.
+ // The initial dot is ignored, as it is always added later in the templates.
+ int lastDotIndex = activityName.lastIndexOf('.');
+
+ if (lastDotIndex != -1) {
+
+ // Resource class
+ if (lastDotIndex > 0) {
+ resourcePackageClass = packageName + '.' + SdkConstants.FN_RESOURCE_BASE;
+ }
+
+ // Package name
+ if (activityName.startsWith(".")) { //$NON-NLS-1$
+ packageName += activityName.substring(0, lastDotIndex);
+ } else {
+ packageName = activityName.substring(0, lastDotIndex);
+ }
+
+ // Activity Class name
+ activityName = activityName.substring(lastDotIndex + 1);
+ }
+
+ java_activity_parameters.put(PARAM_ACTIVITY, activityName);
+ java_activity_parameters.put(PARAM_PACKAGE, packageName);
+ if (resourcePackageClass != null) {
+ String importResourceClass = "\nimport " + resourcePackageClass + ";"; //$NON-NLS-1$ // $NON-NLS-2$
+ java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, importResourceClass);
+ }
+ }
+
+ String[] components = packageName.split(AdtConstants.RE_DOT);
+ for (String component : components) {
+ pkgFolder = pkgFolder.getFolder(component);
+ if (!pkgFolder.exists()) {
+ pkgFolder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ if (activityName != null) {
+ // create the main activity Java file
+ String activityJava = activityName + SdkConstants.DOT_JAVA;
+ IFile file = pkgFolder.getFile(activityJava);
+ if (!file.exists()) {
+ copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor, false);
+ }
+
+ // create the layout file (if we're creating an
+ IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY);
+ file = layoutfolder.getFile(MAIN_LAYOUT_XML);
+ if (!file.exists()) {
+ copyFile(LAYOUT_TEMPLATE, file, parameters, monitor, true);
+ dictionary.put(STRING_HELLO_WORLD, String.format("Hello World, %1$s!",
+ activityName));
+ }
+ }
+ }
+
+ private void copySampleCode(IProject project, File sampleDir,
+ Map<String, Object> parameters, Map<String, String> dictionary,
+ IProgressMonitor monitor) throws CoreException {
+ // Copy the sampleDir into the project directory recursively
+ IFileSystem fileSystem = EFS.getLocalFileSystem();
+ IFileStore sourceDir = new ReadWriteFileStore(
+ fileSystem.getStore(sampleDir.toURI()));
+ IFileStore destDir = new ReadWriteFileStore(
+ fileSystem.getStore(AdtUtils.getAbsolutePath(project)));
+ sourceDir.copy(destDir, EFS.OVERWRITE, null);
+ }
+
+ /**
+ * In a sample we never duplicate source files as read-only.
+ * This creates a store that read files attributes and doesn't set the r-o flag.
+ */
+ private static class ReadWriteFileStore extends FileStoreAdapter {
+
+ public ReadWriteFileStore(IFileStore store) {
+ super(store);
+ }
+
+ // Override when reading attributes
+ @Override
+ public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException {
+ IFileInfo info = super.fetchInfo(options, monitor);
+ info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false);
+ return info;
+ }
+
+ // Override when writing attributes
+ @Override
+ public void putInfo(IFileInfo info, int options, IProgressMonitor storeMonitor)
+ throws CoreException {
+ info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false);
+ super.putInfo(info, options, storeMonitor);
+ }
+
+ @Deprecated
+ @Override
+ public IFileStore getChild(IPath path) {
+ IFileStore child = super.getChild(path);
+ if (!(child instanceof ReadWriteFileStore)) {
+ child = new ReadWriteFileStore(child);
+ }
+ return child;
+ }
+
+ @Override
+ public IFileStore getChild(String name) {
+ return new ReadWriteFileStore(super.getChild(name));
+ }
+ }
+
+ /**
+ * Adds a file to the root of the project
+ * @param project the project to add the file to.
+ * @param destName the name to write the file as
+ * @param source the file to add. It'll keep the same filename once copied into the project.
+ * @param monitor the monitor to report progress to
+ * @throws FileNotFoundException if the file to be added does not exist
+ * @throws CoreException if writing the file does not work
+ */
+ public static void addLocalFile(IProject project, File source, String destName,
+ IProgressMonitor monitor) throws FileNotFoundException, CoreException {
+ IFile dest = project.getFile(destName);
+ if (dest.exists() == false) {
+ FileInputStream stream = new FileInputStream(source);
+ dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /**
+ * Adds the given folder to the project's class path.
+ *
+ * @param javaProject The Java Project to update.
+ * @param sourceFolders Template Parameters.
+ * @param monitor An existing monitor.
+ * @throws JavaModelException if the classpath could not be set.
+ */
+ private void setupSourceFolders(IJavaProject javaProject, String[] sourceFolders,
+ IProgressMonitor monitor) throws JavaModelException {
+ IProject project = javaProject.getProject();
+
+ // get the list of entries.
+ IClasspathEntry[] entries = javaProject.getRawClasspath();
+
+ // remove the project as a source folder (This is the default)
+ entries = removeSourceClasspath(entries, project);
+
+ // add the source folders.
+ for (String sourceFolder : sourceFolders) {
+ IFolder srcFolder = project.getFolder(sourceFolder);
+
+ // remove it first in case.
+ entries = removeSourceClasspath(entries, srcFolder);
+ entries = ProjectHelper.addEntryToClasspath(entries,
+ JavaCore.newSourceEntry(srcFolder.getFullPath()));
+ }
+
+ javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10));
+ }
+
+
+ /**
+ * Removes the corresponding source folder from the class path entries if
+ * found.
+ *
+ * @param entries The class path entries to read. A copy will be returned.
+ * @param folder The parent source folder to remove.
+ * @return A new class path entries array.
+ */
+ private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) {
+ if (folder == null) {
+ return entries;
+ }
+ IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath());
+ int n = entries.length;
+ for (int i = n - 1; i >= 0; i--) {
+ if (entries[i].equals(source)) {
+ IClasspathEntry[] newEntries = new IClasspathEntry[n - 1];
+ if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i);
+ if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1);
+ n--;
+ entries = newEntries;
+ }
+ }
+ return entries;
+ }
+
+
+ /**
+ * Copies the given file from our resource folder to the new project.
+ * Expects the file to the US-ASCII or UTF-8 encoded.
+ *
+ * @throws CoreException from IFile if failing to create the new file.
+ * @throws MalformedURLException from URL if failing to interpret the URL.
+ * @throws FileNotFoundException from RandomAccessFile.
+ * @throws IOException from RandomAccessFile.length() if can't determine the
+ * length.
+ */
+ private void copyFile(String resourceFilename, IFile destFile,
+ Map<String, Object> parameters, IProgressMonitor monitor, boolean reformat)
+ throws CoreException, IOException {
+
+ // Read existing file.
+ String template = AdtPlugin.readEmbeddedTextFile(
+ TEMPLATES_DIRECTORY + resourceFilename);
+
+ // Replace all keyword parameters
+ template = replaceParameters(template, parameters);
+
+ if (reformat) {
+ // Guess the formatting style based on the file location
+ XmlFormatStyle style = EclipseXmlPrettyPrinter
+ .getForFile(destFile.getProjectRelativePath());
+ if (style != null) {
+ template = reformat(style, template);
+ }
+ }
+
+ // Save in the project as UTF-8
+ InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$
+ destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+
+ /**
+ * Replaces placeholders found in a string with values.
+ *
+ * @param str the string to search for placeholders.
+ * @param parameters a map of <placeholder, Value> to search for in the string
+ * @return A new String object with the placeholder replaced by the values.
+ */
+ private String replaceParameters(String str, Map<String, Object> parameters) {
+
+ if (parameters == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "NPW replace parameters: null parameter map. String: '%s'", str); //$NON-NLS-1$
+ return str;
+ } else if (str == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "NPW replace parameters: null template string"); //$NON-NLS-1$
+ return str;
+ }
+
+ for (Entry<String, Object> entry : parameters.entrySet()) {
+ if (entry != null && entry.getValue() instanceof String) {
+ Object value = entry.getValue();
+ if (value == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "NPW replace parameters: null value for key '%s' in template '%s'", //$NON-NLS-1$
+ entry.getKey(),
+ str);
+ } else {
+ str = str.replaceAll(entry.getKey(), (String) value);
+ }
+ }
+ }
+
+ return str;
+ }
+
+ private static class WorksetAdder implements Runnable {
+ private final IJavaProject mProject;
+ private final IWorkingSet[] mWorkingSets;
+
+ private WorksetAdder(IJavaProject project, IWorkingSet[] workingSets) {
+ mProject = project;
+ mWorkingSets = workingSets;
+ }
+
+ @Override
+ public void run() {
+ if (mWorkingSets.length > 0 && mProject != null
+ && mProject.exists()) {
+ PlatformUI.getWorkbench().getWorkingSetManager()
+ .addToWorkingSets(mProject, mWorkingSets);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java
new file mode 100644
index 000000000..ff03b338f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
+import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+
+import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+
+
+/**
+ * A "New Android Project" Wizard.
+ * <p/>
+ * 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.
+ * <p/>
+ * Do not derive from this class.
+ */
+public class NewProjectWizard extends Wizard implements INewWizard {
+ private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$
+
+ private NewProjectWizardState mValues;
+ private ProjectNamePage mNamePage;
+ private SdkSelectionPage mSdkPage;
+ private SampleSelectionPage mSamplePage;
+ private ApplicationInfoPage mPropertiesPage;
+ private final Mode mMode;
+ private IStructuredSelection mSelection;
+
+ /** Constructs a new wizard default project wizard */
+ public NewProjectWizard() {
+ this(Mode.ANY);
+ }
+
+ protected NewProjectWizard(Mode mode) {
+ mMode = mode;
+ switch (mMode) {
+ case SAMPLE:
+ setWindowTitle("New Android Sample Project");
+ break;
+ case TEST:
+ setWindowTitle("New Android Test Project");
+ break;
+ default:
+ setWindowTitle("New Android Project");
+ break;
+ }
+ }
+
+ @Override
+ public void addPages() {
+ mValues = new NewProjectWizardState(mMode);
+
+ if (mMode != Mode.SAMPLE) {
+ mNamePage = new ProjectNamePage(mValues);
+
+ if (mSelection != null) {
+ mNamePage.init(mSelection, AdtUtils.getActivePart());
+ }
+
+ addPage(mNamePage);
+ }
+
+ if (mMode == Mode.TEST) {
+ addPage(new TestTargetPage(mValues));
+ }
+
+ mSdkPage = new SdkSelectionPage(mValues);
+ addPage(mSdkPage);
+
+ if (mMode != Mode.TEST) {
+ // Sample projects can be created when entering the new/existing wizard, or
+ // the sample wizard
+ mSamplePage = new SampleSelectionPage(mValues);
+ addPage(mSamplePage);
+ }
+
+ if (mMode != Mode.SAMPLE) {
+ // Project properties are entered in all project types except sample projects
+ mPropertiesPage = new ApplicationInfoPage(mValues);
+ addPage(mPropertiesPage);
+ }
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ mSelection = selection;
+
+ setHelpAvailable(false); // TODO have help
+ ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ @Override
+ public boolean performFinish() {
+ File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator
+ + FN_PROJECT_PROGUARD_FILE);
+ if (!file.exists()) {
+ AdtPlugin.displayError("Tools Out of Date?",
+ String.format("It looks like you do not have the latest version of the "
+ + "SDK Tools installed. Make sure you update via the SDK Manager "
+ + "first. (Could not find %1$s)", file.getPath()));
+ return false;
+ }
+
+ NewProjectCreator creator = new NewProjectCreator(mValues, getContainer());
+ if (!(creator.createAndroidProjects())) {
+ return false;
+ }
+
+ // Open the default Java Perspective
+ OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction();
+ action.run();
+ return true;
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mNamePage) {
+ // Skip the test target selection page unless creating a test project
+ if (mValues.mode != Mode.TEST) {
+ return mSdkPage;
+ }
+ } else if (page == mSdkPage) {
+ if (mValues.mode == Mode.SAMPLE) {
+ return mSamplePage;
+ } else if (mValues.mode != Mode.TEST) {
+ return mPropertiesPage;
+ } else {
+ // Done with wizard when creating from existing or creating test projects
+ return null;
+ }
+ } else if (page == mSamplePage) {
+ // Nothing more to be entered for samples
+ return null;
+ }
+
+ return super.getNextPage(page);
+ }
+
+ /**
+ * Returns the package name currently set by the wizard
+ *
+ * @return the current package name, or null
+ */
+ public String getPackageName() {
+ return mValues.packageName;
+ }
+
+ // TBD: Call setDialogSettings etc to store persistent state between wizard invocations.
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java
new file mode 100644
index 000000000..06c0300b7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Activity;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
+import com.android.utils.Pair;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.ui.IWorkingSet;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link NewProjectWizardState} holds the state used by the various pages
+ * in the {@link NewProjectWizard} and its variations, and it can also be used
+ * to pass project information to the {@link NewProjectCreator}.
+ */
+public class NewProjectWizardState {
+ /** The mode to run the wizard in: creating test, or sample, or plain project */
+ public Mode mode;
+
+ /**
+ * If true, the project should be created from an existing codebase (pointed
+ * to by the {@link #projectLocation} or in the case of sample projects, the
+ * {@link #chosenSample}. Otherwise, create a brand new project from scratch.
+ */
+ public boolean useExisting;
+
+ /**
+ * Whether new projects should be created into the default project location
+ * (e.g. in the Eclipse workspace) or not
+ */
+ public boolean useDefaultLocation = true;
+
+ /** The build target SDK */
+ public IAndroidTarget target;
+ /** True if the user has manually modified the target */
+ public boolean targetModifiedByUser;
+
+ /** The location to store projects into */
+ public File projectLocation = new File(Platform.getLocation().toOSString());
+ /** True if the project location name has been manually edited by the user */
+ public boolean projectLocationModifiedByUser;
+
+ /** The name of the project */
+ public String projectName = ""; //$NON-NLS-1$
+ /** True if the project name has been manually edited by the user */
+ public boolean projectNameModifiedByUser;
+
+ /** The application name */
+ public String applicationName;
+ /** True if the application name has been manually edited by the user */
+ public boolean applicationNameModifiedByUser;
+
+ /** The package path */
+ public String packageName;
+ /** True if the package name has been manually edited by the user */
+ public boolean packageNameModifiedByUser;
+
+ /** True if a new activity should be created */
+ public boolean createActivity;
+
+ /** The name of the new activity to be created */
+ public String activityName;
+ /** True if the activity name has been manually edited by the user */
+ public boolean activityNameModifiedByUser;
+
+ /** The minimum SDK version to use with the project (may be null or blank) */
+ public String minSdk;
+ /** True if the minimum SDK version has been manually edited by the user */
+ public boolean minSdkModifiedByUser;
+ /**
+ * A list of paths to each of the available samples for the current SDK.
+ * The pair is (String: sample display name => File: sample directory).
+ * Note we want a list, not a map since we might have duplicates.
+ * */
+ public List<Pair<String, File>> samples = new ArrayList<Pair<String, File>>();
+ /** Path to the currently chosen sample */
+ public File chosenSample;
+
+ /** The name of the source folder, relative to the project root */
+ public String sourceFolder = SdkConstants.FD_SOURCES;
+ /** The set of chosen working sets to use when creating the project */
+ public IWorkingSet[] workingSets = new IWorkingSet[0];
+
+ /**
+ * A reference to a different project that the current test project will be
+ * testing.
+ */
+ public IProject testedProject;
+ /**
+ * If true, this test project should be testing itself, otherwise it will be
+ * testing the project pointed to by {@link #testedProject}.
+ */
+ public boolean testingSelf;
+
+ // NOTE: These apply only to creating paired projects; when isTest is true
+ // we're using
+ // the normal fields above
+ /**
+ * If true, create a test project along with this plain project which will
+ * be testing the plain project. (This flag only applies when creating
+ * normal projects.)
+ */
+ public boolean createPairProject;
+ /**
+ * The application name of the test application (only applies when
+ * {@link #createPairProject} is true)
+ */
+ public String testApplicationName;
+ /**
+ * True if the testing application name has been modified by the user (only
+ * applies when {@link #createPairProject} is true)
+ */
+ public boolean testApplicationNameModified;
+ /**
+ * The package name of the test application (only applies when
+ * {@link #createPairProject} is true)
+ */
+ public String testPackageName;
+ /**
+ * True if the testing package name has been modified by the user (only
+ * applies when {@link #createPairProject} is true)
+ */
+ public boolean testPackageModified;
+ /**
+ * The project name of the test project (only applies when
+ * {@link #createPairProject} is true)
+ */
+ public String testProjectName;
+ /**
+ * True if the testing project name has been modified by the user (only
+ * applies when {@link #createPairProject} is true)
+ */
+ public boolean testProjectModified;
+ /** Package name of the tested app */
+ public String testTargetPackageName;
+
+ /**
+ * Copy project into workspace? This flag only applies when importing
+ * projects (creating projects from existing source)
+ */
+ public boolean copyIntoWorkspace;
+
+ /**
+ * List of projects to be imported. Null if not importing projects.
+ */
+ @Nullable
+ public List<ImportedProject> importProjects;
+
+ /**
+ * Creates a new {@link NewProjectWizardState}
+ *
+ * @param mode the mode to run the wizard in
+ */
+ public NewProjectWizardState(Mode mode) {
+ this.mode = mode;
+ if (mode == Mode.SAMPLE) {
+ useExisting = true;
+ } else if (mode == Mode.TEST) {
+ createActivity = false;
+ }
+ }
+
+ /**
+ * Extract information (package name, application name, minimum SDK etc) from
+ * the given Android project.
+ *
+ * @param path the path to the project to extract information from
+ */
+ public void extractFromAndroidManifest(Path path) {
+ String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString();
+ if (!(new File(osPath).exists())) {
+ return;
+ }
+
+ ManifestData manifestData = AndroidManifestHelper.parseForData(osPath);
+ if (manifestData == null) {
+ return;
+ }
+
+ String newPackageName = null;
+ Activity activity = null;
+ String newActivityName = null;
+ String minSdkVersion = null;
+ try {
+ newPackageName = manifestData.getPackage();
+ minSdkVersion = manifestData.getMinSdkVersionString();
+
+ // 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 (newPackageName != null && newPackageName.length() > 0) {
+ packageName = newPackageName;
+ }
+
+ if (activity != null) {
+ newActivityName = AndroidManifest.extractActivityName(activity.getName(),
+ newPackageName);
+ }
+
+ if (newActivityName != null && newActivityName.length() > 0) {
+ activityName = newActivityName;
+ // we are "importing" an existing activity, not creating a new one
+ createActivity = 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 (newActivityName.indexOf('.') != -1) {
+ String[] ids = newActivityName.split(AdtConstants.RE_DOT);
+ newActivityName = ids[ids.length - 1];
+ }
+ if (projectName == null || projectName.length() == 0 ||
+ !projectNameModifiedByUser) {
+ projectName = newActivityName;
+ projectNameModifiedByUser = false;
+ }
+ if (applicationName == null || applicationName.length() == 0 ||
+ !applicationNameModifiedByUser) {
+ applicationNameModifiedByUser = false;
+ applicationName = newActivityName;
+ }
+ } else {
+ activityName = ""; //$NON-NLS-1$
+
+ // 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 (newPackageName != null && newPackageName.length() > 0) {
+ // Package name is a java identifier, so it's most suitable for
+ // an application name.
+
+ if (applicationName == null || applicationName.length() == 0 ||
+ !applicationNameModifiedByUser) {
+ applicationName = newPackageName;
+ }
+
+ // For the project name, remove any dots
+ newPackageName = newPackageName.replace('.', '_');
+ if (projectName == null || projectName.length() == 0 ||
+ !projectNameModifiedByUser) {
+ projectName = newPackageName;
+ }
+
+ }
+ }
+
+ if (mode == Mode.ANY && useExisting) {
+ updateSdkTargetToMatchProject(path.toFile());
+ }
+
+ minSdk = minSdkVersion;
+ minSdkModifiedByUser = false;
+ }
+
+ /**
+ * Try to find an SDK Target that matches the current MinSdkVersion.
+ *
+ * There can be multiple targets with the same sdk api version, so don't change
+ * it if it's already at the right version. Otherwise pick the first target
+ * that matches.
+ */
+ public void updateSdkTargetToMatchMinSdkVersion() {
+ IAndroidTarget currentTarget = target;
+ if (currentTarget != null && currentTarget.getVersion().equals(minSdk)) {
+ return;
+ }
+
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ IAndroidTarget[] targets = sdk.getTargets();
+ for (IAndroidTarget t : targets) {
+ if (t.getVersion().equals(minSdk)) {
+ target = t;
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the SDK to reflect the SDK required by the project at the given
+ * location
+ *
+ * @param location the location of the project
+ */
+ public void updateSdkTargetToMatchProject(File location) {
+ // Select the target matching the manifest's sdk or build properties, if any
+ IAndroidTarget foundTarget = null;
+ // This is the target currently in the UI
+ IAndroidTarget currentTarget = target;
+ String projectPath = location.getPath();
+
+ // If there's a current target defined, we do not allow to change it when
+ // operating in the create-from-sample mode -- since the available sample list
+ // is tied to the current target, so changing it would invalidate the project we're
+ // trying to load in the first place.
+ if (!targetModifiedByUser) {
+ ProjectProperties p = ProjectProperties.load(projectPath,
+ PropertyType.PROJECT);
+ if (p != null) {
+ String v = p.getProperty(ProjectProperties.PROPERTY_TARGET);
+ IAndroidTarget desiredTarget = Sdk.getCurrent().getTargetFromHashString(v);
+ // We can change the current target if:
+ // - we found a new desired target
+ // - there is no current target
+ // - or the current target can't run the desired target
+ if (desiredTarget != null &&
+ (currentTarget == null || !desiredTarget.canRunOn(currentTarget))) {
+ foundTarget = desiredTarget;
+ }
+ }
+
+ Sdk sdk = Sdk.getCurrent();
+ IAndroidTarget[] targets = null;
+ if (sdk != null) {
+ targets = sdk.getTargets();
+ }
+ if (targets == null) {
+ targets = new IAndroidTarget[0];
+ }
+
+ if (foundTarget == null && minSdk != null) {
+ // Otherwise try to match the requested min-sdk-version if we find an
+ // exact match, regardless of the currently selected target.
+ for (IAndroidTarget existingTarget : targets) {
+ if (existingTarget != null &&
+ existingTarget.getVersion().equals(minSdk)) {
+ foundTarget = existingTarget;
+ break;
+ }
+ }
+ }
+
+ if (foundTarget == null) {
+ // Or last attempt, try to match a sample project location and use it
+ // if we find an exact match, regardless of the currently selected target.
+ for (IAndroidTarget existingTarget : targets) {
+ if (existingTarget != null &&
+ projectPath.startsWith(existingTarget.getLocation())) {
+ foundTarget = existingTarget;
+ break;
+ }
+ }
+ }
+ }
+
+ if (foundTarget != null) {
+ target = foundTarget;
+ }
+ }
+
+ /**
+ * Type of project being offered/created by the wizard
+ */
+ public enum Mode {
+ /** Create a sample project. Testing options are not presented. */
+ SAMPLE,
+
+ /**
+ * Create a test project, either testing itself or some other project.
+ * Note that even if in the {@link #ANY} mode, a test project can be
+ * created as a *paired* project with the main project, so this flag
+ * only means that we are creating *just* a test project
+ */
+ TEST,
+
+ /**
+ * Create an Android project, which can be a plain project, optionally
+ * with a paired test project, or a sample project (the first page
+ * contains toggles for choosing which
+ */
+ ANY;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java
new file mode 100644
index 000000000..6b6a4c29e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+
+/**
+ * A "New Sample Android Project" Wizard.
+ * <p/>
+ * This displays the new project wizard pre-configured for samples only.
+ */
+public class NewSampleProjectWizard extends NewProjectWizard {
+ /**
+ * Creates a new wizard for creating a sample Android project
+ */
+ public NewSampleProjectWizard() {
+ super(Mode.SAMPLE);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java
new file mode 100644
index 000000000..e0959f4db
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+
+/**
+ * A "New Test Android Project" Wizard.
+ * <p/>
+ * This is really the {@link NewProjectWizard} that only displays the "test project" pages.
+ */
+public class NewTestProjectWizard extends NewProjectWizard {
+ /**
+ * Creates a new wizard for creating an Android Test Project
+ */
+ public NewTestProjectWizard() {
+ super(Mode.TEST);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java
new file mode 100644
index 000000000..d04ea897f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
+import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
+import static com.android.ide.eclipse.adt.AdtUtils.capitalize;
+import static com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage.ACTIVITY_NAME_SUFFIX;
+import static com.android.utils.SdkUtils.stripWhitespace;
+
+import com.android.SdkConstants;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Activity;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.VersionCheck;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+
+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.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.osgi.util.TextProcessor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkingSet;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Locale;
+
+/**
+ * Initial page shown when creating projects which asks for the project name,
+ * the the location of the project, working sets, etc.
+ */
+public class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener {
+ private final NewProjectWizardState mValues;
+ /** Flag used when setting button/text state manually to ignore listener updates */
+ private boolean mIgnore;
+ /** 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 Text mProjectNameText;
+ private Text mLocationText;
+ private Button mCreateSampleRadioButton;
+ private Button mCreateNewButton;
+ private Button mUseDefaultCheckBox;
+ private Button mBrowseButton;
+ private Label mLocationLabel;
+ private WorkingSetGroup mWorkingSetGroup;
+ /**
+ * Whether we've made sure the Tools are up to date (enough that all the
+ * resources required by the New Project wizard are present -- we don't
+ * necessarily check for newer versions than that here; that's done by
+ * {@link VersionCheck}, though that check doesn't <b>enforce</b> an update
+ * since it needs to allow the user to proceed to access the SDK manager
+ * etc.)
+ */
+ private boolean mCheckedSdkUptodate;
+
+ /**
+ * Create the wizard.
+ * @param values current wizard state
+ */
+ ProjectNamePage(NewProjectWizardState values) {
+ super("projectNamePage"); //$NON-NLS-1$
+ mValues = values;
+
+ setTitle("Create Android Project");
+ setDescription("Select project name and type of project");
+ mWorkingSetGroup = new WorkingSetGroup();
+ setWorkingSets(new IWorkingSet[0]);
+ }
+
+ void init(IStructuredSelection selection, IWorkbenchPart activePart) {
+ setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
+ }
+
+ /**
+ * Create contents of the wizard.
+ * @param parent the parent to add the page to
+ */
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ container.setLayout(new GridLayout(3, false));
+
+ Label nameLabel = new Label(container, SWT.NONE);
+ nameLabel.setText("Project Name:");
+
+ mProjectNameText = new Text(container, SWT.BORDER);
+ mProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+ mProjectNameText.addModifyListener(this);
+
+ if (mValues.mode != Mode.TEST) {
+ mCreateNewButton = new Button(container, SWT.RADIO);
+ mCreateNewButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+ mCreateNewButton.setText("Create new project in workspace");
+ mCreateNewButton.addSelectionListener(this);
+
+ // TBD: Should we hide this completely, and make samples something you only invoke
+ // from the "New Sample Project" wizard?
+ mCreateSampleRadioButton = new Button(container, SWT.RADIO);
+ mCreateSampleRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false,
+ 3, 1));
+ mCreateSampleRadioButton.setText("Create project from existing sample");
+ mCreateSampleRadioButton.addSelectionListener(this);
+ }
+
+ Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
+ separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1));
+
+ mUseDefaultCheckBox = new Button(container, SWT.CHECK);
+ mUseDefaultCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+ mUseDefaultCheckBox.setText("Use default location");
+ mUseDefaultCheckBox.addSelectionListener(this);
+
+ mLocationLabel = new Label(container, SWT.NONE);
+ mLocationLabel.setText("Location:");
+
+ mLocationText = new Text(container, SWT.BORDER);
+ mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mLocationText.addModifyListener(this);
+
+ mBrowseButton = new Button(container, SWT.NONE);
+ mBrowseButton.setText("Browse...");
+ mBrowseButton.addSelectionListener(this);
+
+ Composite group = mWorkingSetGroup.createControl(container);
+ group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1));
+
+ setControl(container);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ if (visible) {
+ try {
+ mIgnore = true;
+ if (mValues.projectName != null) {
+ mProjectNameText.setText(mValues.projectName);
+ mProjectNameText.setFocus();
+ }
+ if (mValues.mode == Mode.ANY || mValues.mode == Mode.TEST) {
+ if (mValues.useExisting) {
+ assert false; // This is now handled by the separate import wizard
+ } else if (mCreateNewButton != null) {
+ mCreateNewButton.setSelection(true);
+ }
+ } else if (mValues.mode == Mode.SAMPLE) {
+ mCreateSampleRadioButton.setSelection(true);
+ }
+ if (mValues.projectLocation != null) {
+ mLocationText.setText(mValues.projectLocation.getPath());
+ }
+ mUseDefaultCheckBox.setSelection(mValues.useDefaultLocation);
+ updateLocationState();
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+
+ if (source == mProjectNameText) {
+ onProjectFieldModified();
+ if (!mValues.useDefaultLocation && !mValues.projectLocationModifiedByUser) {
+ updateLocationPathField(null);
+ }
+ } else if (source == mLocationText) {
+ mValues.projectLocationModifiedByUser = true;
+ if (!mValues.useDefaultLocation) {
+ File f = new File(mLocationText.getText().trim());
+ mValues.projectLocation = f;
+ if (f.exists() && f.isDirectory() && !f.equals(mValues.projectLocation)) {
+ updateLocationPathField(mValues.projectLocation.getPath());
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ private void onProjectFieldModified() {
+ mValues.projectName = mProjectNameText.getText().trim();
+ mValues.projectNameModifiedByUser = true;
+
+ if (!mValues.applicationNameModifiedByUser) {
+ mValues.applicationName = capitalize(mValues.projectName);
+ if (!mValues.testApplicationNameModified) {
+ mValues.testApplicationName =
+ ApplicationInfoPage.suggestTestApplicationName(mValues.applicationName);
+ }
+ }
+ if (!mValues.activityNameModifiedByUser) {
+ String name = capitalize(mValues.projectName);
+ mValues.activityName = stripWhitespace(name) + ACTIVITY_NAME_SUFFIX;
+ }
+ if (!mValues.testProjectModified) {
+ mValues.testProjectName =
+ ApplicationInfoPage.suggestTestProjectName(mValues.projectName);
+ }
+ if (!mValues.projectLocationModifiedByUser) {
+ updateLocationPathField(null);
+ }
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+
+ if (source == mCreateNewButton && mCreateNewButton != null
+ && mCreateNewButton.getSelection()) {
+ mValues.useExisting = false;
+ if (mValues.mode == Mode.SAMPLE) {
+ // Only reset the mode if we're toggling from sample back to create new
+ // or create existing. We can only come to the sample state when we're in
+ // ANY mode. (In particular, we don't want to switch to ANY if you're
+ // in test mode.
+ mValues.mode = Mode.ANY;
+ }
+ updateLocationState();
+ } else if (source == mCreateSampleRadioButton && mCreateSampleRadioButton.getSelection()) {
+ mValues.useExisting = true;
+ mValues.useDefaultLocation = true;
+ if (!mUseDefaultCheckBox.getSelection()) {
+ try {
+ mIgnore = true;
+ mUseDefaultCheckBox.setSelection(true);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ mValues.mode = Mode.SAMPLE;
+ updateLocationState();
+ } else if (source == mUseDefaultCheckBox) {
+ mValues.useDefaultLocation = mUseDefaultCheckBox.getSelection();
+ updateLocationState();
+ } else if (source == mBrowseButton) {
+ onOpenDirectoryBrowser();
+ }
+
+ validatePage();
+ }
+
+ /**
+ * 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 updateLocationState() {
+ boolean isNewProject = !mValues.useExisting;
+ boolean isCreateFromSample = mValues.mode == Mode.SAMPLE;
+ boolean useDefault = mValues.useDefaultLocation && !isCreateFromSample;
+ boolean locationEnabled = (!isNewProject || !useDefault) && !isCreateFromSample;
+
+ mUseDefaultCheckBox.setEnabled(isNewProject);
+ mLocationLabel.setEnabled(locationEnabled);
+ mLocationText.setEnabled(locationEnabled);
+ mBrowseButton.setEnabled(locationEnabled);
+
+ updateLocationPathField(null);
+ }
+
+ /**
+ * Display a directory browser and update the location path field with the selected path
+ */
+ private void onOpenDirectoryBrowser() {
+
+ String existingDir = mLocationText.getText().trim();
+
+ // Disable the path if it doesn't exist
+ if (existingDir.length() == 0) {
+ existingDir = null;
+ } else {
+ File f = new File(existingDir);
+ if (!f.exists()) {
+ existingDir = null;
+ }
+ }
+
+ DirectoryDialog directoryDialog = new DirectoryDialog(mLocationText.getShell());
+ directoryDialog.setMessage("Browse for folder");
+ directoryDialog.setFilterPath(existingDir);
+ String dir = directoryDialog.open();
+
+ if (dir != null) {
+ updateLocationPathField(dir);
+ validatePage();
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ /**
+ * Returns the working sets to which the new project should be added.
+ *
+ * @return the selected working sets to which the new project should be added
+ */
+ private IWorkingSet[] getWorkingSets() {
+ return mWorkingSetGroup.getSelectedWorkingSets();
+ }
+
+ /**
+ * Sets the working sets to which the new project should be added.
+ *
+ * @param workingSets the initial selected working sets
+ */
+ private void setWorkingSets(IWorkingSet[] workingSets) {
+ assert workingSets != null;
+ mWorkingSetGroup.setWorkingSets(workingSets);
+ }
+
+ /**
+ * Updates the location directory path field.
+ * <br/>
+ * When custom user selection is enabled, use the absDir argument if not null and also
+ * save it internally. If absDir is null, restore the last saved absDir. 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 absDir.
+ *
+ * @param absDir A new absolute directory path or null to use the default.
+ */
+ private void updateLocationPathField(String absDir) {
+ boolean isNewProject = !mValues.useExisting || mValues.mode == Mode.SAMPLE;
+ boolean useDefault = mValues.useDefaultLocation;
+ boolean customLocation = !isNewProject || !useDefault;
+
+ if (!mIgnore) {
+ try {
+ mIgnore = true;
+ if (customLocation) {
+ if (absDir != 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 &&
+ absDir.equals(sCustomLocationOsPath);
+ sCustomLocationOsPath = TextProcessor.process(absDir);
+ } else if (sAutoComputeCustomLocation ||
+ (!isNewProject && !new File(sCustomLocationOsPath).isDirectory())) {
+ // As a default import location, just suggest the home directory; the user
+ // needs to point to a project to import.
+ // TODO: Open file chooser automatically?
+ sCustomLocationOsPath = System.getProperty("user.home"); //$NON-NLS-1$
+ }
+ if (!mLocationText.getText().equals(sCustomLocationOsPath)) {
+ mLocationText.setText(sCustomLocationOsPath);
+ mValues.projectLocation = new File(sCustomLocationOsPath);
+ }
+ } else {
+ String value = Platform.getLocation().append(mValues.projectName).toString();
+ value = TextProcessor.process(value);
+ if (!mLocationText.getText().equals(value)) {
+ mLocationText.setText(value);
+ mValues.projectLocation = new File(value);
+ }
+ }
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ if (mValues.useExisting && mValues.projectLocation != null
+ && mValues.projectLocation.exists() && mValues.mode != Mode.SAMPLE) {
+ mValues.extractFromAndroidManifest(new Path(mValues.projectLocation.getPath()));
+ if (!mValues.projectNameModifiedByUser && mValues.projectName != null) {
+ try {
+ mIgnore = true;
+ mProjectNameText.setText(mValues.projectName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+ }
+
+ private void validatePage() {
+ IStatus status = null;
+
+ // Validate project name -- unless we're creating a sample, in which case
+ // the user will get a chance to pick the name on the Sample page
+ if (mValues.mode != Mode.SAMPLE) {
+ status = validateProjectName(mValues.projectName);
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ IStatus validLocation = validateLocation();
+ if (validLocation != null) {
+ status = validLocation;
+ }
+ }
+
+ if (!mCheckedSdkUptodate) {
+ // Ensure that we have a recent enough version of the Tools that the right templates
+ // are available
+ File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER
+ + File.separator + FN_PROJECT_PROGUARD_FILE);
+ if (!file.exists()) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("You do not have the latest version of the "
+ + "SDK Tools installed: Please update. (Missing %1$s)", file.getPath()));
+ } else {
+ mCheckedSdkUptodate = true;
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ private IStatus validateLocation() {
+ if (mValues.mode == Mode.SAMPLE) {
+ // Samples are always created in the default directory
+ return null;
+ }
+
+ // Validate location
+ Path path = new Path(mValues.projectLocation.getPath());
+ if (!mValues.useExisting) {
+ if (!mValues.useDefaultLocation) {
+ // If not using the default value validate the location.
+ URI uri = URIUtil.toURI(path.toOSString());
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IProject handle = workspace.getRoot().getProject(mValues.projectName);
+ IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri);
+ if (!locationStatus.isOK()) {
+ return locationStatus;
+ }
+ // 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 new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "A directory name must be specified.");
+ } 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 != null && l.length != 0) {
+ return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "The selected output directory is not empty.");
+ }
+ }
+ } else {
+ // Otherwise validate the path string is not empty
+ if (mValues.projectLocation.getPath().length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "A directory name must be specified.");
+ }
+ File dest = path.toFile();
+ if (dest.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "There is already a file or directory named \"%1$s\" in the selected location.",
+ mValues.projectName));
+ }
+ }
+ } else {
+ // Must be an existing directory
+ File f = path.toFile();
+ if (!f.isDirectory()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "An existing directory name must be specified.");
+ }
+
+ // Check there's an android manifest in the directory
+ String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString();
+ File manifestFile = new File(osPath);
+ if (!manifestFile.isFile()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "Choose a valid Android code directory\n" +
+ "(%1$s not found in %2$s.)",
+ SdkConstants.FN_ANDROID_MANIFEST_XML, f.getName()));
+ }
+
+ // Parse it and check the important fields.
+ ManifestData manifestData = AndroidManifestHelper.parseForData(osPath);
+ if (manifestData == null) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("File %1$s could not be parsed.", osPath));
+ }
+ String packageName = manifestData.getPackage();
+ if (packageName == null || packageName.length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("No package name defined in %1$s.", osPath));
+ }
+ Activity[] activities = manifestData.getActivities();
+ if (activities == null || activities.length == 0) {
+ // This is acceptable now as long as no activity needs to be
+ // created
+ if (mValues.createActivity) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("No activity name defined in %1$s.", osPath));
+ }
+ }
+
+ // If there's already a .project, tell the user to use import instead.
+ if (path.append(".project").toFile().exists()) { //$NON-NLS-1$
+ return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "An Eclipse project already exists in this directory.\n" +
+ "Consider using File > Import > Existing Project instead.");
+ }
+ }
+
+ return null;
+ }
+
+ public static IStatus validateProjectName(String projectName) {
+ if (projectName == null || projectName.length() == 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Project name must be specified");
+ } else {
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT);
+ if (!nameStatus.isOK()) {
+ return nameStatus;
+ } else {
+ // Note: the case-sensitiveness of the project name matters and can cause a
+ // conflict *later* when creating the project resource, so let's check it now.
+ for (IProject existingProj : workspace.getRoot().getProjects()) {
+ if (projectName.equalsIgnoreCase(existingProj.getName())) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "A project with that name already exists in the workspace");
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ // Sync working set data to the value object, since the WorkingSetGroup
+ // doesn't let us add listeners to do this lazily
+ mValues.workingSets = getWorkingSets();
+
+ return super.getNextPage();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java
new file mode 100644
index 000000000..197247083
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/** Page where the user can select a sample to "instantiate" */
+class SampleSelectionPage extends WizardPage implements SelectionListener, ModifyListener {
+ private final NewProjectWizardState mValues;
+ private boolean mIgnore;
+
+ private Table mTable;
+ private TableViewer mTableViewer;
+ private IAndroidTarget mCurrentSamplesTarget;
+ private Text mSampleProjectName;
+
+ /**
+ * Create the wizard.
+ */
+ SampleSelectionPage(NewProjectWizardState values) {
+ super("samplePage"); //$NON-NLS-1$
+ setTitle("Select Sample");
+ setDescription("Select which sample to create");
+ mValues = values;
+ }
+
+ /**
+ * Create contents of the wizard.
+ */
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ container.setLayout(new GridLayout(2, false));
+
+ mTableViewer = new TableViewer(container, SWT.BORDER | SWT.FULL_SELECTION);
+ mTable = mTableViewer.getTable();
+ GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
+ gridData.heightHint = 300;
+ mTable.setLayoutData(gridData);
+ mTable.addSelectionListener(this);
+
+ setControl(container);
+
+ Label projectLabel = new Label(container, SWT.NONE);
+ projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ projectLabel.setText("Project Name:");
+
+ mSampleProjectName = new Text(container, SWT.BORDER);
+ mSampleProjectName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mSampleProjectName.addModifyListener(this);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ if (visible) {
+ if (mValues.projectName != null) {
+ try {
+ mIgnore = true;
+ mSampleProjectName.setText(mValues.projectName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ // Update samples list if the SDK target has changed (or if it hasn't yet
+ // been populated)
+ if (mCurrentSamplesTarget != mValues.target) {
+ mCurrentSamplesTarget = mValues.target;
+ updateSamples();
+ }
+
+ validatePage();
+ }
+ }
+
+ private void updateSamples() {
+ IBaseLabelProvider labelProvider = new ColumnLabelProvider() {
+ @Override
+ public Image getImage(Object element) {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public String getText(Object element) {
+ if (element instanceof Pair<?, ?>) {
+ Object name = ((Pair<?, ?>) element).getFirst();
+ return name.toString();
+ }
+ return element.toString(); // Fallback. Should not happen.
+ }
+ };
+
+ mTableViewer.setContentProvider(new ArrayContentProvider());
+ mTableViewer.setLabelProvider(labelProvider);
+
+ if (mValues.samples != null && mValues.samples.size() > 0) {
+ Object[] samples = mValues.samples.toArray();
+ mTableViewer.setInput(samples);
+
+ mTable.select(0);
+ selectSample(mValues.samples.get(0).getSecond());
+ extractNamesFromAndroidManifest();
+ }
+ }
+
+ private void selectSample(File sample) {
+ mValues.chosenSample = sample;
+ if (sample != null && !mValues.projectNameModifiedByUser) {
+ mValues.projectName = sample.getName();
+ if (SdkConstants.FD_SAMPLE.equals(mValues.projectName) &&
+ sample.getParentFile() != null) {
+ mValues.projectName = sample.getParentFile().getName() + '_' + mValues.projectName;
+ }
+ try {
+ mIgnore = true;
+ mSampleProjectName.setText(mValues.projectName);
+ } finally {
+ mIgnore = false;
+ }
+ updatedProjectName();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ if (e.getSource() == mTable) {
+ extractNamesFromAndroidManifest();
+ int index = mTable.getSelectionIndex();
+ if (index >= 0) {
+ Object[] roots = (Object[]) mTableViewer.getInput();
+ selectSample(((Pair<String, File>) roots[index]).getSecond());
+ } else {
+ selectSample(null);
+ }
+ } else {
+ assert false : e.getSource();
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ if (e.getSource() == mSampleProjectName) {
+ mValues.projectName = mSampleProjectName.getText().trim();
+ mValues.projectNameModifiedByUser = true;
+ updatedProjectName();
+ }
+
+ validatePage();
+ }
+
+ private void updatedProjectName() {
+ if (mValues.useDefaultLocation) {
+ mValues.projectLocation = Platform.getLocation().toFile();
+ }
+ }
+
+ /**
+ * A sample was selected. Update the location field, manifest and validate.
+ * 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 (mValues.chosenSample == null || !mValues.chosenSample.isDirectory()) {
+ return;
+ }
+
+ Path path = new Path(mValues.chosenSample.getPath());
+ mValues.extractFromAndroidManifest(path);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ if (mValues.mode != Mode.SAMPLE) {
+ return true;
+ }
+
+ // Ensure that when creating samples, the Finish button isn't enabled until
+ // the user has reached and completed this page
+ if (mValues.chosenSample == null) {
+ return false;
+ }
+
+ return super.isPageComplete();
+ }
+
+ private void validatePage() {
+ IStatus status = null;
+ if (mValues.samples == null || mValues.samples.size() == 0) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "The chosen SDK does not contain any samples");
+ } else if (mValues.chosenSample == null) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Choose a sample");
+ } else if (!mValues.chosenSample.exists()) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Sample does not exist: %1$s", mValues.chosenSample.getPath()));
+ } else {
+ status = ProjectNamePage.validateProjectName(mValues.projectName);
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java
new file mode 100644
index 000000000..6cafcf057
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.common.xml.AndroidManifestParser;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
+import com.android.io.FileWrapper;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkManager;
+import com.android.sdkuilib.internal.widgets.SdkTargetSelector;
+import com.android.utils.NullLogger;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+/** A page in the New Project wizard where you select the target SDK */
+class SdkSelectionPage extends WizardPage implements ITargetChangeListener {
+ private final NewProjectWizardState mValues;
+ private boolean mIgnore;
+ private SdkTargetSelector mSdkTargetSelector;
+
+ /**
+ * Create the wizard.
+ */
+ SdkSelectionPage(NewProjectWizardState values) {
+ super("sdkSelection"); //$NON-NLS-1$
+ mValues = values;
+
+ setTitle("Select Build Target");
+ AdtPlugin.getDefault().addTargetListener(this);
+ }
+
+ @Override
+ public void dispose() {
+ AdtPlugin.getDefault().removeTargetListener(this);
+ super.dispose();
+ }
+
+ /**
+ * Create contents of the wizard.
+ */
+ @Override
+ public void createControl(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);
+
+ mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ mValues.target = mSdkTargetSelector.getSelected();
+ mValues.targetModifiedByUser = true;
+ onSdkTargetModified();
+ validatePage();
+ }
+ });
+
+ onSdkLoaded();
+
+ setControl(group);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (mValues.mode == Mode.SAMPLE) {
+ setDescription("Choose an SDK to select a sample from");
+ } else {
+ setDescription("Choose an SDK to target");
+ }
+ try {
+ mIgnore = true;
+ if (mValues.target != null) {
+ mSdkTargetSelector.setSelection(mValues.target);
+ }
+ } finally {
+ mIgnore = false;
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ // Ensure that the Finish button isn't enabled until
+ // the user has reached and completed this page
+ if (mValues.target == null) {
+ return false;
+ }
+
+ return super.isPageComplete();
+ }
+
+ /**
+ * Called when an SDK target is modified.
+ *
+ * Also changes the minSdkVersion field to reflect the sdk api level that has
+ * just been selected.
+ */
+ private void onSdkTargetModified() {
+ if (mIgnore) {
+ return;
+ }
+
+ IAndroidTarget target = mValues.target;
+
+ // Update the minimum SDK text field?
+ // We do if one of two conditions are met:
+ if (target != null) {
+ boolean setMinSdk = false;
+ AndroidVersion version = target.getVersion();
+ int apiLevel = version.getApiLevel();
+ // 1. Has the user not manually edited the SDK field yet? If so, keep
+ // updating it to the selected value.
+ if (!mValues.minSdkModifiedByUser) {
+ setMinSdk = true;
+ } else {
+ // 2. Is the API level set to a higher level than the newly selected
+ // target SDK? If so, change it down to the new lower value.
+ String s = mValues.minSdk;
+ if (s.length() > 0) {
+ try {
+ int currentApi = Integer.parseInt(s);
+ if (currentApi > apiLevel) {
+ setMinSdk = true;
+ }
+ } catch (NumberFormatException nfe) {
+ // User may have typed something invalid -- ignore
+ }
+ }
+ }
+ if (setMinSdk) {
+ String minSdk;
+ if (version.isPreview()) {
+ minSdk = version.getCodename();
+ } else {
+ minSdk = Integer.toString(apiLevel);
+ }
+ mValues.minSdk = minSdk;
+ }
+ }
+
+ loadSamplesForTarget(target);
+ }
+
+ /**
+ * Updates the list of all samples for the given target SDK.
+ * The list is stored in mSamplesPaths as absolute directory paths.
+ * The combo is recreated to match this.
+ */
+ private void loadSamplesForTarget(IAndroidTarget target) {
+ // Keep the name of the old selection (if there were any samples)
+ File previouslyChosenSample = mValues.chosenSample;
+
+ mValues.samples.clear();
+ mValues.chosenSample = null;
+
+ if (target != null) {
+ // Get the sample root path and recompute the list of samples
+ String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES);
+
+ File root = new File(samplesRootPath);
+ findSamplesManifests(root, root, null, null, mValues.samples);
+
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ // Parse the extras to see if we can find samples that are
+ // compatible with the selected target API.
+ // First we need an SdkManager that suppresses all output.
+ SdkManager sdkman = sdk.getNewSdkManager(NullLogger.getLogger());
+
+ Map<File, String> extras = sdkman.getExtraSamples();
+ for (Entry<File, String> entry : extras.entrySet()) {
+ File path = entry.getKey();
+ String name = entry.getValue();
+
+ // Case where the sample is at the root of the directory and not
+ // in a per-sample sub-directory.
+ if (path.getName().equals(SdkConstants.FD_SAMPLE)) {
+ findSampleManifestInDir(
+ path, path, name, target.getVersion(), mValues.samples);
+ }
+
+ // Scan sub-directories
+ findSamplesManifests(
+ path, path, name, target.getVersion(), mValues.samples);
+ }
+ }
+
+ if (mValues.samples.isEmpty()) {
+ return;
+ } else {
+ Collections.sort(mValues.samples, new Comparator<Pair<String, File>>() {
+ @Override
+ public int compare(Pair<String, File> o1, Pair<String, File> o2) {
+ // Compare the display name of the sample
+ return o1.getFirst().compareTo(o2.getFirst());
+ }
+ });
+ }
+
+ // Try to find the old selection.
+ if (previouslyChosenSample != null) {
+ String previouslyChosenName = previouslyChosenSample.getName();
+ for (int i = 0, n = mValues.samples.size(); i < n; i++) {
+ File file = mValues.samples.get(i).getSecond();
+ if (file.getName().equals(previouslyChosenName)) {
+ mValues.chosenSample = file;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Recursively find potential sample directories under the given directory.
+ * Actually lists any directory that contains an android manifest.
+ * Paths found are added the samplesPaths list.
+ *
+ * @param rootDir The "samples" root directory. Doesn't change during recursion.
+ * @param currDir The directory being scanned. Caller must initially set it to {@code rootDir}.
+ * @param extraName Optional name appended to the samples display name. Typically used to
+ * indicate a sample comes from a given extra package.
+ * @param targetVersion Optional target version filter. If non null, only samples that are
+ * compatible with the given target will be listed.
+ * @param samplesPaths A non-null list filled by this method with all samples found. The
+ * pair is (String: sample display name => File: sample directory).
+ */
+ private void findSamplesManifests(
+ File rootDir,
+ File currDir,
+ @Nullable String extraName,
+ @Nullable AndroidVersion targetVersion,
+ List<Pair<String, File>> samplesPaths) {
+ if (!currDir.isDirectory()) {
+ return;
+ }
+
+ for (File f : currDir.listFiles()) {
+ if (f.isDirectory()) {
+ findSampleManifestInDir(f, rootDir, extraName, targetVersion, samplesPaths);
+
+ // Recurse in the project, to find embedded tests sub-projects
+ // We can however skip this recursion for known android sub-dirs that
+ // can't have projects, namely for sources, assets and resources.
+ String leaf = f.getName();
+ if (!SdkConstants.FD_SOURCES.equals(leaf) &&
+ !SdkConstants.FD_ASSETS.equals(leaf) &&
+ !SdkConstants.FD_RES.equals(leaf)) {
+ findSamplesManifests(rootDir, f, extraName, targetVersion, samplesPaths);
+ }
+ }
+ }
+ }
+
+ private void findSampleManifestInDir(
+ File sampleDir,
+ File rootDir,
+ String extraName,
+ AndroidVersion targetVersion,
+ List<Pair<String, File>> samplesPaths) {
+ // Assume this is a sample if it contains an android manifest.
+ File manifestFile = new File(sampleDir, SdkConstants.FN_ANDROID_MANIFEST_XML);
+ if (manifestFile.isFile()) {
+ try {
+ ManifestData data =
+ AndroidManifestParser.parse(new FileWrapper(manifestFile));
+ if (data != null) {
+ boolean accept = false;
+ if (targetVersion == null) {
+ accept = true;
+ } else if (targetVersion != null) {
+ int i = data.getMinSdkVersion();
+ if (i != ManifestData.MIN_SDK_CODENAME) {
+ accept = i <= targetVersion.getApiLevel();
+ } else {
+ String s = data.getMinSdkVersionString();
+ if (s != null) {
+ accept = s.equals(targetVersion.getCodename());
+ }
+ }
+ }
+
+ if (accept) {
+ String name = getSampleDisplayName(extraName, rootDir, sampleDir);
+ samplesPaths.add(Pair.of(name, sampleDir));
+ }
+ }
+ } catch (Exception e) {
+ // Ignore. Don't use a sample which manifest doesn't parse correctly.
+ AdtPlugin.log(IStatus.INFO,
+ "NPW ignoring malformed manifest %s", //$NON-NLS-1$
+ manifestFile.getAbsolutePath());
+ }
+ }
+ }
+
+ /**
+ * Compute the sample name compared to its root directory.
+ */
+ private String getSampleDisplayName(String extraName, File rootDir, File sampleDir) {
+ String name = null;
+ if (!rootDir.equals(sampleDir)) {
+ String path = sampleDir.getPath();
+ int n = rootDir.getPath().length();
+ if (path.length() > n) {
+ path = path.substring(n);
+ if (path.charAt(0) == File.separatorChar) {
+ path = path.substring(1);
+ }
+ if (path.endsWith(File.separator)) {
+ path = path.substring(0, path.length() - 1);
+ }
+ name = path.replaceAll(Pattern.quote(File.separator), " > "); //$NON-NLS-1$
+ }
+ }
+ if (name == null &&
+ rootDir.equals(sampleDir) &&
+ sampleDir.getName().equals(SdkConstants.FD_SAMPLE) &&
+ extraName != null) {
+ // This is an old-style extra with one single sample directory. Just use the
+ // extra's name as the same name.
+ return extraName;
+ }
+ if (name == null) {
+ // Otherwise try to use the sample's directory name as the sample name.
+ while (sampleDir != null &&
+ (name == null ||
+ SdkConstants.FD_SAMPLE.equals(name) ||
+ SdkConstants.FD_SAMPLES.equals(name))) {
+ name = sampleDir.getName();
+ sampleDir = sampleDir.getParentFile();
+ }
+ }
+ if (name == null) {
+ if (extraName != null) {
+ // In the unlikely case nothing worked and we have an extra name, use that.
+ return extraName;
+ } else {
+ name = "Sample"; // fallback name... should not happen. //$NON-NLS-1$
+ }
+ }
+ if (extraName != null) {
+ name = name + " [" + extraName + ']'; //$NON-NLS-1$
+ }
+
+ return name;
+ }
+
+ private void validatePage() {
+ String error = null;
+
+ if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADING) {
+ error = "The SDK is still loading; please wait.";
+ }
+
+ if (error == null && mValues.target == null) {
+ error = "An SDK Target must be specified.";
+ }
+
+ if (error == null && mValues.mode == Mode.SAMPLE) {
+ // Make sure this SDK target contains samples
+ if (mValues.samples == null || mValues.samples.size() == 0) {
+ error = "This target has no samples. Please select another target.";
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, IMessageProvider.ERROR);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ // ---- Implements ITargetChangeListener ----
+ @Override
+ public void onSdkLoaded() {
+ if (mSdkTargetSelector == null) {
+ return;
+ }
+
+ // 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.
+ // This will invoke the selection listener on the selector defined above.
+ if (targets != null && targets.length == 1) {
+ mValues.target = targets[0];
+ mSdkTargetSelector.setSelection(mValues.target);
+ onSdkTargetModified();
+ } else if (targets != null) {
+ // Pick the highest available platform by default (see issue #17505
+ // for related discussion.)
+ IAndroidTarget initialTarget = null;
+ for (IAndroidTarget target : targets) {
+ if (target.isPlatform()
+ && !target.getVersion().isPreview()
+ && (initialTarget == null ||
+ target.getVersion().getApiLevel() >
+ initialTarget.getVersion().getApiLevel())) {
+ initialTarget = target;
+ }
+ }
+ if (initialTarget != null) {
+ mValues.target = initialTarget;
+ try {
+ mIgnore = true;
+ mSdkTargetSelector.setSelection(mValues.target);
+ } finally {
+ mIgnore = false;
+ }
+ onSdkTargetModified();
+ }
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void onProjectTargetChange(IProject changedProject) {
+ // Ignore
+ }
+
+ @Override
+ public void onTargetLoaded(IAndroidTarget target) {
+ // Ignore
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java
new file mode 100644
index 000000000..f1c188ae9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+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.ui.dialogs.FilteredList;
+
+/**
+ * Page shown when creating a test project which lets you choose between testing
+ * yourself and testing a different project
+ */
+class TestTargetPage extends WizardPage implements SelectionListener {
+ private final NewProjectWizardState mValues;
+ /** Flag used when setting button/text state manually to ignore listener updates */
+ private boolean mIgnore;
+ private String mLastExistingPackageName;
+
+ private Button mCurrentRadioButton;
+ private Button mExistingRadioButton;
+ private FilteredList mProjectList;
+ private boolean mPageShown;
+
+ /**
+ * Create the wizard.
+ */
+ TestTargetPage(NewProjectWizardState values) {
+ super("testTargetPage"); //$NON-NLS-1$
+ setTitle("Select Test Target");
+ setDescription("Choose a project to test");
+ mValues = values;
+ }
+
+ /**
+ * Create contents of the wizard.
+ */
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+
+ setControl(container);
+ container.setLayout(new GridLayout(2, false));
+
+ mCurrentRadioButton = new Button(container, SWT.RADIO);
+ mCurrentRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ mCurrentRadioButton.setText("This project");
+ mCurrentRadioButton.addSelectionListener(this);
+
+ mExistingRadioButton = new Button(container, SWT.RADIO);
+ mExistingRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ mExistingRadioButton.setText("An existing Android project:");
+ mExistingRadioButton.addSelectionListener(this);
+
+ ILabelProvider labelProvider = new JavaElementLabelProvider(
+ JavaElementLabelProvider.SHOW_DEFAULT);
+ mProjectList = new FilteredList(container,
+ SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE, labelProvider,
+ true /*ignoreCase*/, false /*allowDuplicates*/, true /* matchEmptyString*/);
+ mProjectList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+ mProjectList.addSelectionListener(this);
+ }
+
+ private void initializeList() {
+ ProjectChooserHelper helper = new ProjectChooserHelper(getShell(), null /*filter*/);
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+ IJavaProject[] androidProjects = helper.getAndroidProjects(javaModel);
+ mProjectList.setElements(androidProjects);
+ if (mValues.testedProject != null) {
+ for (IJavaProject project : androidProjects) {
+ if (project.getProject() == mValues.testedProject) {
+ mProjectList.setSelection(new Object[] { project });
+ break;
+ }
+ }
+ } else {
+ // No initial selection: force the user to choose
+ mProjectList.setSelection(new int[0]);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ mPageShown = true;
+
+ if (visible) {
+ try {
+ mIgnore = true;
+ mCurrentRadioButton.setSelection(mValues.testingSelf);
+ mExistingRadioButton.setSelection(!mValues.testingSelf);
+ mProjectList.setEnabled(!mValues.testingSelf);
+
+ if (mProjectList.isEmpty()) {
+ initializeList();
+ }
+ if (!mValues.testingSelf) {
+ mProjectList.setFocus();
+ IProject project = getSelectedProject();
+ if (project != null) {
+ // The FilteredList seems to -insist- on selecting the first item
+ // in the list, even when the selection is explicitly set to an empty
+ // array. This means the user is looking at a selection, so we need
+ // to also go ahead and select this item in the model such that the
+ // two agree, even if we would have preferred to have no initial
+ // selection.
+ mValues.testedProject = project;
+ }
+ }
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mExistingRadioButton) {
+ mProjectList.setEnabled(true);
+ mValues.testingSelf = false;
+ setExistingProject(getSelectedProject());
+ mProjectList.setFocus();
+ } else if (source == mCurrentRadioButton) {
+ mProjectList.setEnabled(false);
+ mValues.testingSelf = true;
+ mValues.testedProject = null;
+ } else {
+ // The event must be from the project list, which unfortunately doesn't
+ // pass itself as the selection event, it passes a reference to some internal
+ // table widget that it uses, so we check for this case last
+ IProject project = getSelectedProject();
+ if (project != mValues.testedProject) {
+ setExistingProject(project);
+ }
+ }
+
+ validatePage();
+ }
+
+ private IProject getSelectedProject() {
+ Object[] selection = mProjectList.getSelection();
+ IProject project = selection != null && selection.length == 1
+ ? ((IJavaProject) selection[0]).getProject() : null;
+ return project;
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ private void setExistingProject(IProject project) {
+ mValues.testedProject = project;
+
+ // Try to update the application, package, sdk target and minSdkVersion accordingly
+ if (project != null &&
+ (!mValues.applicationNameModifiedByUser ||
+ !mValues.packageNameModifiedByUser ||
+ !mValues.targetModifiedByUser ||
+ !mValues.minSdkModifiedByUser)) {
+ ManifestData manifestData = AndroidManifestHelper.parseForData(project);
+ if (manifestData != null) {
+ String appName = String.format("%1$sTest", project.getName());
+ String packageName = manifestData.getPackage();
+ String minSdkVersion = manifestData.getMinSdkVersionString();
+ IAndroidTarget sdkTarget = null;
+ if (Sdk.getCurrent() != null) {
+ sdkTarget = Sdk.getCurrent().getTarget(project);
+ }
+
+ if (packageName == null) {
+ packageName = ""; //$NON-NLS-1$
+ }
+ mLastExistingPackageName = packageName;
+
+ if (!mValues.projectNameModifiedByUser) {
+ mValues.projectName = appName;
+ }
+
+ if (!mValues.applicationNameModifiedByUser) {
+ mValues.applicationName = appName;
+ }
+
+ if (!mValues.packageNameModifiedByUser) {
+ packageName += ".test"; //$NON-NLS-1$
+ mValues.packageName = packageName;
+ }
+
+ if (!mValues.targetModifiedByUser && sdkTarget != null) {
+ mValues.target = sdkTarget;
+ }
+
+ if (!mValues.minSdkModifiedByUser) {
+ if (minSdkVersion != null || sdkTarget != null) {
+ mValues.minSdk = minSdkVersion;
+ }
+ if (sdkTarget == null) {
+ mValues.updateSdkTargetToMatchMinSdkVersion();
+ }
+ }
+ }
+ }
+
+ updateTestTargetPackageField(mLastExistingPackageName);
+ }
+
+ /**
+ * Updates the test target package name
+ *
+ * When using the "self-test" option, the packageName argument is ignored and the
+ * current value from the project package is used.
+ *
+ * Otherwise the packageName is used if it is not null.
+ */
+ private void updateTestTargetPackageField(String packageName) {
+ if (mValues.testingSelf) {
+ mValues.testTargetPackageName = mValues.packageName;
+ } else if (packageName != null) {
+ mValues.testTargetPackageName = packageName;
+ }
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ // Ensure that the user sees the page and makes a selection
+ if (!mPageShown) {
+ return false;
+ }
+
+ return super.isPageComplete();
+ }
+
+ private void validatePage() {
+ String error = null;
+
+ if (!mValues.testingSelf) {
+ if (mValues.testedProject == null) {
+ error = "Please select an existing Android project as a test target.";
+ } else if (mValues.projectName.equals(mValues.testedProject.getName())) {
+ error = "The main project name and the test project name must be different.";
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, IMessageProvider.ERROR);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java
new file mode 100644
index 000000000..fb33a08b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2009 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
+import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.ui.IWorkingSet;
+import org.eclipse.ui.dialogs.WorkingSetConfigurationBlock;
+
+/**
+ * Copied from
+ * org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne$WorkingSetGroup
+ *
+ * Creates the working set group with controls that allow
+ * the selection of working sets
+ */
+@SuppressWarnings("restriction")
+public class WorkingSetGroup {
+
+ private WorkingSetConfigurationBlock fWorkingSetBlock;
+ private Button mEnableButton;
+
+ public WorkingSetGroup() {
+ String[] workingSetIds = new String[] {
+ IWorkingSetIDs.JAVA, IWorkingSetIDs.RESOURCE
+ };
+ fWorkingSetBlock = new WorkingSetConfigurationBlock(workingSetIds, JavaPlugin.getDefault()
+ .getDialogSettings());
+ }
+
+ public Composite createControl(Composite composite) {
+ Group workingSetGroup = new Group(composite, SWT.NONE);
+ workingSetGroup.setFont(composite.getFont());
+ workingSetGroup.setText(NewWizardMessages.NewJavaProjectWizardPageOne_WorkingSets_group);
+ workingSetGroup.setLayout(new GridLayout(1, false));
+
+ fWorkingSetBlock.createContent(workingSetGroup);
+
+ // WorkingSetGroup is implemented in such a way that the checkbox it contains
+ // can only be programmatically set if there's an existing working set associated
+ // *before* we construct the control. However the control is created when the
+ // wizard is opened, not when the page is first shown.
+ //
+ // One choice is to duplicate the class in our project.
+ // Or find the checkbox we want and trigger it manually.
+ mEnableButton = findCheckbox(workingSetGroup);
+
+ return workingSetGroup;
+ }
+
+ public void setWorkingSets(IWorkingSet[] workingSets) {
+ fWorkingSetBlock.setWorkingSets(workingSets);
+ }
+
+ public IWorkingSet[] getSelectedWorkingSets() {
+ try {
+ return fWorkingSetBlock.getSelectedWorkingSets();
+ } catch (Throwable t) {
+ // Test scenarios; no UI is created, which the fWorkingSetBlock assumes
+ // (it dereferences the enabledButton)
+ return new IWorkingSet[0];
+ }
+ }
+
+ public boolean isChecked() {
+ return mEnableButton == null ? false : mEnableButton.getSelection();
+ }
+
+ public void setChecked(boolean state) {
+ if (mEnableButton != null) {
+ mEnableButton.setSelection(state);
+ }
+ }
+
+ /**
+ * Finds the first button of style Checkbox in the given parent composite.
+ * Returns null if not found.
+ */
+ private Button findCheckbox(Composite parent) {
+ for (Control control : parent.getChildren()) {
+ if (control instanceof Button && (control.getStyle() & SWT.CHECK) == SWT.CHECK) {
+ return (Button) control;
+ } else if (control instanceof Composite) {
+ Button found = findCheckbox((Composite) control);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java
new file mode 100755
index 000000000..428bfd331
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2009 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+
+package com.android.ide.eclipse.adt.internal.wizards.newproject;
+
+import org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart;
+import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkingSet;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * This class contains a helper method to deal with working sets.
+ * <p/>
+ * Copied from org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne
+ */
+@SuppressWarnings("restriction")
+public final class WorkingSetHelper {
+
+ private static final IWorkingSet[] EMPTY_WORKING_SET_ARRAY = new IWorkingSet[0];
+
+ /** This class is never instantiated. */
+ private WorkingSetHelper() {
+ }
+
+ public static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection,
+ IWorkbenchPart activePart) {
+ IWorkingSet[] selected= getSelectedWorkingSet(selection);
+ if (selected != null && selected.length > 0) {
+ for (int i= 0; i < selected.length; i++) {
+ if (!isValidWorkingSet(selected[i]))
+ return EMPTY_WORKING_SET_ARRAY;
+ }
+ return selected;
+ }
+
+ if (!(activePart instanceof PackageExplorerPart))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ PackageExplorerPart explorerPart= (PackageExplorerPart) activePart;
+ if (explorerPart.getRootMode() == PackageExplorerPart.PROJECTS_AS_ROOTS) {
+ //Get active filter
+ IWorkingSet filterWorkingSet= explorerPart.getFilterWorkingSet();
+ if (filterWorkingSet == null)
+ return EMPTY_WORKING_SET_ARRAY;
+
+ if (!isValidWorkingSet(filterWorkingSet))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ return new IWorkingSet[] {filterWorkingSet};
+ } else {
+ //If we have been gone into a working set return the working set
+ Object input= explorerPart.getViewPartInput();
+ if (!(input instanceof IWorkingSet))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ IWorkingSet workingSet= (IWorkingSet)input;
+ if (!isValidWorkingSet(workingSet))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ return new IWorkingSet[] {workingSet};
+ }
+ }
+
+ private static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection) {
+ if (!(selection instanceof ITreeSelection))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ ITreeSelection treeSelection= (ITreeSelection) selection;
+ if (treeSelection.isEmpty())
+ return EMPTY_WORKING_SET_ARRAY;
+
+ List<?> elements = treeSelection.toList();
+ if (elements.size() == 1) {
+ Object element= elements.get(0);
+ TreePath[] paths= treeSelection.getPathsFor(element);
+ if (paths.length != 1)
+ return EMPTY_WORKING_SET_ARRAY;
+
+ TreePath path= paths[0];
+ if (path.getSegmentCount() == 0)
+ return EMPTY_WORKING_SET_ARRAY;
+
+ Object candidate= path.getSegment(0);
+ if (!(candidate instanceof IWorkingSet))
+ return EMPTY_WORKING_SET_ARRAY;
+
+ IWorkingSet workingSetCandidate= (IWorkingSet) candidate;
+ if (isValidWorkingSet(workingSetCandidate))
+ return new IWorkingSet[] { workingSetCandidate };
+
+ return EMPTY_WORKING_SET_ARRAY;
+ }
+
+ ArrayList<Object> result = new ArrayList<Object>();
+ for (Iterator<?> iterator = elements.iterator(); iterator.hasNext();) {
+ Object element= iterator.next();
+ if (element instanceof IWorkingSet && isValidWorkingSet((IWorkingSet) element)) {
+ result.add(element);
+ }
+ }
+ return result.toArray(new IWorkingSet[result.size()]);
+ }
+
+
+ private static boolean isValidWorkingSet(IWorkingSet workingSet) {
+ String id= workingSet.getId();
+ if (!IWorkingSetIDs.JAVA.equals(id) && !IWorkingSetIDs.RESOURCE.equals(id))
+ return false;
+
+ if (workingSet.isAggregateWorkingSet())
+ return false;
+
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java
new file mode 100644
index 000000000..0301b80fe
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
+
+import static com.android.SdkConstants.FD_RES;
+import static com.android.SdkConstants.FD_RES_VALUES;
+import static com.android.SdkConstants.RES_QUALIFIER_SEP;
+
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.res2.ValueXmlHelper;
+import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.FlagManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceType;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.ColumnViewer;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+
+/**
+ * Dialog which adds a new translation to the project
+ */
+public class AddTranslationDialog extends Dialog implements ControlListener, SelectionListener,
+ TraverseListener {
+ private static final int KEY_COLUMN = 0;
+ private static final int DEFAULT_TRANSLATION_COLUMN = 1;
+ private static final int NEW_TRANSLATION_COLUMN = 2;
+ private final FolderConfiguration mConfiguration = new FolderConfiguration();
+ private final IProject mProject;
+ private String mTarget;
+ private boolean mIgnore;
+ private Map<String, String> mTranslations;
+ private Set<String> mExistingLanguages;
+ private String mSelectedLanguage;
+ private String mSelectedRegion;
+
+ private Table mTable;
+ private Combo mLanguageCombo;
+ private Combo mRegionCombo;
+ private ImageControl mFlag;
+ private Label mFile;
+ private Button mOkButton;
+ private Composite mErrorPanel;
+ private Label mErrorLabel;
+ private MyTableViewer mTableViewer;
+
+ /**
+ * Creates the dialog.
+ * @param parentShell the parent shell
+ * @param project the project to add translations into
+ */
+ public AddTranslationDialog(Shell parentShell, IProject project) {
+ super(parentShell);
+ setShellStyle(SWT.CLOSE | SWT.RESIZE | SWT.TITLE);
+ mProject = project;
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite container = (Composite) super.createDialogArea(parent);
+ GridLayout gl_container = new GridLayout(6, false);
+ gl_container.horizontalSpacing = 0;
+ container.setLayout(gl_container);
+
+ Label languageLabel = new Label(container, SWT.NONE);
+ languageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ languageLabel.setText("Language:");
+ mLanguageCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gd_mLanguageCombo = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1);
+ gd_mLanguageCombo.widthHint = 150;
+ mLanguageCombo.setLayoutData(gd_mLanguageCombo);
+
+ Label regionLabel = new Label(container, SWT.NONE);
+ GridData gd_regionLabel = new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1);
+ gd_regionLabel.horizontalIndent = 10;
+ regionLabel.setLayoutData(gd_regionLabel);
+ regionLabel.setText("Region:");
+ mRegionCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gd_mRegionCombo = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
+ gd_mRegionCombo.widthHint = 150;
+ mRegionCombo.setLayoutData(gd_mRegionCombo);
+ mRegionCombo.setEnabled(false);
+
+ mFlag = new ImageControl(container, SWT.NONE, null);
+ mFlag.setDisposeImage(false);
+ GridData gd_mFlag = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
+ gd_mFlag.exclude = true;
+ gd_mFlag.widthHint = 32;
+ gd_mFlag.horizontalIndent = 3;
+ mFlag.setLayoutData(gd_mFlag);
+
+ mFile = new Label(container, SWT.NONE);
+ mFile.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+ mTableViewer = new MyTableViewer(container, SWT.BORDER | SWT.FULL_SELECTION);
+ mTable = mTableViewer.getTable();
+ mTable.setEnabled(false);
+ mTable.setLinesVisible(true);
+ mTable.setHeaderVisible(true);
+ mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 6, 2));
+ mTable.addControlListener(this);
+ mTable.addTraverseListener(this);
+ // If you have difficulty opening up this form in WindowBuilder and it complains about
+ // the next line, change the type of the mTableViewer field and the above
+ // constructor call from MyTableViewer to TableViewer
+ TableViewerColumn keyViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
+ TableColumn keyColumn = keyViewerColumn.getColumn();
+ keyColumn.setWidth(100);
+ keyColumn.setText("Key");
+ TableViewerColumn defaultViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
+ TableColumn defaultColumn = defaultViewerColumn.getColumn();
+ defaultColumn.setWidth(200);
+ defaultColumn.setText("Default");
+ TableViewerColumn translationViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
+ TableColumn translationColumn = translationViewerColumn.getColumn();
+ translationColumn.setWidth(200);
+ translationColumn.setText("New Translation");
+
+ mErrorPanel = new Composite(container, SWT.NONE);
+ GridData gd_mErrorLabel = new GridData(SWT.FILL, SWT.CENTER, false, false, 6, 1);
+ gd_mErrorLabel.exclude = true;
+ mErrorPanel.setLayoutData(gd_mErrorLabel);
+
+ translationViewerColumn.setEditingSupport(new TranslationEditingSupport(mTableViewer));
+
+ fillLanguages();
+ fillRegions();
+ fillStrings();
+ updateColumnWidths();
+ validatePage();
+
+ mLanguageCombo.addSelectionListener(this);
+ mRegionCombo.addSelectionListener(this);
+
+ return container;
+ }
+
+ /** Populates the table with keys and default strings */
+ private void fillStrings() {
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(mProject);
+ mExistingLanguages = resources.getLanguages();
+
+ Collection<ResourceItem> items = resources.getResourceItemsOfType(ResourceType.STRING);
+
+ ResourceItem[] array = items.toArray(new ResourceItem[items.size()]);
+ Arrays.sort(array);
+
+ // TODO: Read in the actual XML files providing the default keys here
+ // (they can be obtained via ResourceItem.getSourceFileList())
+ // such that we can read all the attributes associated with each
+ // item, and if it defines translatable=false, or the filename is
+ // donottranslate.xml, we can ignore it, and in other cases just
+ // duplicate all the attributes (such as "formatted=true", or other
+ // local conventions such as "product=tablet", or "msgid="123123123",
+ // etc.)
+
+ mTranslations = Maps.newHashMapWithExpectedSize(items.size());
+ IBaseLabelProvider labelProvider = new CellLabelProvider() {
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ int index = cell.getColumnIndex();
+ ResourceItem item = (ResourceItem) element;
+ switch (index) {
+ case KEY_COLUMN: {
+ // Key
+ cell.setText(item.getName());
+ return;
+ }
+ case DEFAULT_TRANSLATION_COLUMN: {
+ // Default translation
+ ResourceValue value = item.getResourceValue(ResourceType.STRING,
+ mConfiguration, false);
+
+ if (value != null) {
+ cell.setText(value.getValue());
+ return;
+ }
+ break;
+ }
+ case NEW_TRANSLATION_COLUMN: {
+ // New translation
+ String translation = mTranslations.get(item.getName());
+ if (translation != null) {
+ cell.setText(translation);
+ return;
+ }
+ break;
+ }
+ default:
+ assert false : index;
+ }
+ cell.setText("");
+ }
+ };
+
+ mTableViewer.setLabelProvider(labelProvider);
+ mTableViewer.setContentProvider(new ArrayContentProvider());
+ mTableViewer.setInput(array);
+ }
+
+ /** Populate the languages dropdown */
+ private void fillLanguages() {
+ List<String> languageCodes = LocaleManager.getLanguageCodes();
+ List<String> labels = new ArrayList<String>();
+ for (String code : languageCodes) {
+ labels.add(code + ": " + LocaleManager.getLanguageName(code)); //$NON-NLS-1$
+ }
+ Collections.sort(labels);
+ labels.add(0, "(Select)");
+ mLanguageCombo.setItems(labels.toArray(new String[labels.size()]));
+ mLanguageCombo.select(0);
+ }
+
+ /** Populate the regions dropdown */
+ private void fillRegions() {
+ // TODO: When you switch languages, offer some "default" usable options. For example,
+ // when you choose English, offer the countries that use English, and so on. Unfortunately
+ // we don't have good data about this, we'd just need to hardcode a few common cases.
+ List<String> regionCodes = LocaleManager.getRegionCodes();
+ List<String> labels = new ArrayList<String>();
+ for (String code : regionCodes) {
+ labels.add(code + ": " + LocaleManager.getRegionName(code)); //$NON-NLS-1$
+ }
+ Collections.sort(labels);
+ labels.add(0, "Any");
+ mRegionCombo.setItems(labels.toArray(new String[labels.size()]));
+ mRegionCombo.select(0);
+ }
+
+ /** React to resizing by distributing the space evenly between the last two columns */
+ private void updateColumnWidths() {
+ Rectangle r = mTable.getClientArea();
+ int availableWidth = r.width;
+ // Distribute all available space to the last two columns
+ int columnCount = mTable.getColumnCount();
+ for (int i = 0; i < columnCount; i++) {
+ TableColumn column = mTable.getColumn(i);
+ availableWidth -= column.getWidth();
+ }
+ if (availableWidth != 0) {
+ TableColumn column = mTable.getColumn(DEFAULT_TRANSLATION_COLUMN);
+ column.setWidth(column.getWidth() + availableWidth / 2);
+ column = mTable.getColumn(NEW_TRANSLATION_COLUMN);
+ column.setWidth(column.getWidth() + availableWidth / 2 + availableWidth % 2);
+ }
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL,
+ // Don't make the OK button default as in most dialogs, since when you press
+ // Return thinking you might edit a value it dismisses the dialog instead
+ false);
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+ mOkButton.setEnabled(false);
+
+ validatePage();
+ }
+
+ /**
+ * Return the initial size of the dialog.
+ */
+ @Override
+ protected Point getInitialSize() {
+ return new Point(800, 600);
+ }
+
+ private void updateTarget() {
+ if (mSelectedLanguage == null) {
+ mTarget = null;
+ mFile.setText("");
+ } else {
+ String folder = FD_RES + '/' + FD_RES_VALUES + RES_QUALIFIER_SEP + mSelectedLanguage;
+ if (mSelectedRegion != null) {
+ folder = folder + RES_QUALIFIER_SEP + 'r' + mSelectedRegion;
+ }
+ mTarget = folder + "/strings.xml"; //$NON-NLS-1$
+ mFile.setText(String.format("Creating %1$s", mTarget));
+ }
+ }
+
+ private void updateFlag() {
+ if (mSelectedLanguage == null) {
+ // Nothing selected
+ ((GridData) mFlag.getLayoutData()).exclude = true;
+ } else {
+ FlagManager manager = FlagManager.get();
+ Image flag = manager.getFlag(mSelectedLanguage, mSelectedRegion);
+ if (flag != null) {
+ ((GridData) mFlag.getLayoutData()).exclude = false;
+ mFlag.setImage(flag);
+ }
+ }
+
+ mFlag.getParent().layout(true);
+ mFlag.getParent().redraw();
+ }
+
+ /** Actually create the new translation file and write it to disk */
+ private void createTranslation() {
+ List<String> keys = new ArrayList<String>(mTranslations.keySet());
+ Collections.sort(keys);
+
+ StringBuilder sb = new StringBuilder(keys.size() * 120);
+ sb.append("<resources>\n\n"); //$NON-NLS-1$
+ for (String key : keys) {
+ String value = mTranslations.get(key);
+ if (value == null || value.trim().isEmpty()) {
+ continue;
+ }
+ sb.append(" <string name=\""); //$NON-NLS-1$
+ sb.append(key);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(ValueXmlHelper.escapeResourceString(value));
+ sb.append("</string>\n"); //$NON-NLS-1$
+ }
+ sb.append("\n</resources>"); //$NON-NLS-1$
+
+ IFile file = mProject.getFile(mTarget);
+
+ try {
+ IContainer parent = file.getParent();
+ AdtUtils.ensureExists(parent);
+ InputStream source = new ByteArrayInputStream(sb.toString().getBytes(Charsets.UTF_8));
+ file.create(source, true, new NullProgressMonitor());
+ AdtPlugin.openFile(file, null, true /*showEditorTab*/);
+
+ // Ensure that the project resources updates itself to notice the new language.
+ // In theory, this shouldn't be necessary.
+ ResourceManager manager = ResourceManager.getInstance();
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IFolder folder = root.getFolder(parent.getFullPath());
+ manager.getResourceFolder(folder);
+ RenderPreviewManager.bumpRevision();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private void validatePage() {
+ if (mOkButton == null) { // Early initialization
+ return;
+ }
+
+ String message = null;
+
+ if (mSelectedLanguage == null) {
+ message = "Select a language";
+ } else if (mExistingLanguages.contains(mSelectedLanguage)) {
+ if (mSelectedRegion == null) {
+ message = String.format("%1$s is already translated in this project",
+ LocaleManager.getLanguageName(mSelectedLanguage));
+ } else {
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(mProject);
+ SortedSet<String> regions = resources.getRegions(mSelectedLanguage);
+ if (regions.contains(mSelectedRegion)) {
+ message = String.format("%1$s (%2$s) is already translated in this project",
+ LocaleManager.getLanguageName(mSelectedLanguage),
+ LocaleManager.getRegionName(mSelectedRegion));
+ }
+ }
+ } else {
+ // Require all strings to be translated? No, some of these may not
+ // be translatable (e.g. translatable=false, defined in donottranslate.xml, etc.)
+ //int missing = mTable.getItemCount() - mTranslations.values().size();
+ //if (missing > 0) {
+ // message = String.format("Missing %1$d translations", missing);
+ //}
+ }
+
+ boolean valid = message == null;
+ mTable.setEnabled(message == null);
+ mOkButton.setEnabled(valid);
+ showError(message);
+ }
+
+ private void showError(String error) {
+ GridData data = (GridData) mErrorPanel.getLayoutData();
+
+ boolean show = error != null;
+ if (show == data.exclude) {
+ if (show) {
+ if (mErrorLabel == null) {
+ mErrorPanel.setLayout(new GridLayout(2, false));
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ ISharedImages sharedImages = workbench.getSharedImages();
+ String iconName = ISharedImages.IMG_OBJS_ERROR_TSK;
+ Image image = sharedImages.getImage(iconName);
+ @SuppressWarnings("unused")
+ ImageControl icon = new ImageControl(mErrorPanel, SWT.NONE, image);
+
+ mErrorLabel = new Label(mErrorPanel, SWT.NONE);
+ mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 1, 1));
+ }
+ mErrorLabel.setText(error);
+ }
+ data.exclude = !show;
+ mErrorPanel.getParent().layout(true);
+ }
+ }
+
+ @Override
+ protected void okPressed() {
+ mTableViewer.applyEditorValue();
+
+ super.okPressed();
+ createTranslation();
+ }
+
+ // ---- Implements ControlListener ----
+
+ @Override
+ public void controlMoved(ControlEvent e) {
+ }
+
+ @Override
+ public void controlResized(ControlEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ updateColumnWidths();
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mLanguageCombo) {
+ try {
+ mIgnore = true;
+ mRegionCombo.select(0);
+ mSelectedRegion = null;
+ } finally {
+ mIgnore = false;
+ }
+
+ int languageIndex = mLanguageCombo.getSelectionIndex();
+ if (languageIndex == 0) {
+ mSelectedLanguage = null;
+ mRegionCombo.setEnabled(false);
+ } else {
+ // This depends on the label format
+ mSelectedLanguage = mLanguageCombo.getItem(languageIndex).substring(0, 2);
+ mRegionCombo.setEnabled(true);
+ }
+
+ updateTarget();
+ updateFlag();
+ } else if (source == mRegionCombo) {
+ int regionIndex = mRegionCombo.getSelectionIndex();
+ if (regionIndex == 0) {
+ mSelectedRegion = null;
+ } else {
+ mSelectedRegion = mRegionCombo.getItem(regionIndex).substring(0, 2);
+ }
+
+ updateTarget();
+ updateFlag();
+ }
+
+ try {
+ mIgnore = true;
+ validatePage();
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- TraverseListener ----
+
+ @Override
+ public void keyTraversed(TraverseEvent e) {
+ // If you press Return and we're not cell editing, start editing the current row
+ if (e.detail == SWT.TRAVERSE_RETURN && !mTableViewer.isCellEditorActive()) {
+ int index = mTable.getSelectionIndex();
+ if (index != -1) {
+ Object next = mTable.getItem(index).getData();
+ mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN);
+ }
+ }
+ }
+
+ /** Editing support for the translation column */
+ private class TranslationEditingSupport extends EditingSupport {
+ /**
+ * When true, setValue is being called as part of a default action
+ * (e.g. Return), not due to focus loss
+ */
+ private boolean mDefaultAction;
+
+ private TranslationEditingSupport(ColumnViewer viewer) {
+ super(viewer);
+ }
+
+ @Override
+ protected void setValue(Object element, Object value) {
+ ResourceItem item = (ResourceItem) element;
+ mTranslations.put(item.getName(), value.toString());
+ mTableViewer.update(element, null);
+ validatePage();
+
+ // If the user is pressing Return to finish editing a value (which is
+ // not the only way this method can get called - for example, if you click
+ // outside the cell while editing, the focus loss will also result in
+ // this method getting called), then mDefaultAction is true, and we automatically
+ // start editing the next row.
+ if (mDefaultAction) {
+ mTable.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mTable.isDisposed() && !mTableViewer.isCellEditorActive()) {
+ int index = mTable.getSelectionIndex();
+ if (index != -1 && index < mTable.getItemCount() - 1) {
+ Object next = mTable.getItem(index + 1).getData();
+ mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ protected Object getValue(Object element) {
+ ResourceItem item = (ResourceItem) element;
+ String value = mTranslations.get(item.getName());
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+
+ @Override
+ protected CellEditor getCellEditor(Object element) {
+ return new TextCellEditor(mTable) {
+ @Override
+ protected void handleDefaultSelection(SelectionEvent event) {
+ try {
+ mDefaultAction = true;
+ super.handleDefaultSelection(event);
+ } finally {
+ mDefaultAction = false;
+ }
+ }
+ };
+ }
+
+ @Override
+ protected boolean canEdit(Object element) {
+ return true;
+ }
+ }
+
+ private class MyTableViewer extends TableViewer {
+ public MyTableViewer(Composite parent, int style) {
+ super(parent, style);
+ }
+
+ // Make this public so we can call it to ensure values are applied before the dialog
+ // is dismissed in {@link #okPressed}
+ @Override
+ public void applyEditorValue() {
+ super.applyEditorValue();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java
new file mode 100644
index 000000000..1d6467e64
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard.Values;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Second page of the {@link NewXmlFileWizard}.
+ * <p>
+ * This page is used for choosing the current configuration or specific resource
+ * folder.
+ */
+public class ChooseConfigurationPage extends WizardPage {
+ private Values mValues;
+ private Text mWsFolderPathTextField;
+ private ConfigurationSelector mConfigSelector;
+ private boolean mInternalWsFolderPathUpdate;
+ private boolean mInternalConfigSelectorUpdate;
+
+ /** Absolute destination folder root, e.g. "/res/" */
+ static final String RES_FOLDER_ABS = AdtConstants.WS_RESOURCES + AdtConstants.WS_SEP;
+ /** Relative destination folder root, e.g. "res/" */
+ static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP;
+
+ /**
+ * Create the wizard.
+ *
+ * @param values value object holding current wizard state
+ */
+ public ChooseConfigurationPage(NewXmlFileWizard.Values values) {
+ super("chooseConfig");
+ mValues = values;
+ setTitle("Choose Configuration Folder");
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ if (mValues.folderPath != null) {
+ mWsFolderPathTextField.setText(mValues.folderPath);
+ }
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ // This UI is maintained with WindowBuilder.
+
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayout(new GridLayout(2, false /* makeColumnsEqualWidth */));
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // label before configuration selector
+ Label label = new Label(composite, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ label.setText("Optional: Choose a specific configuration to limit the XML to:");
+
+ // configuration selector
+ mConfigSelector = new ConfigurationSelector(composite, SelectorMode.DEFAULT);
+ GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+ gd.verticalAlignment = SWT.FILL;
+ gd.horizontalAlignment = SWT.FILL;
+ gd.horizontalSpan = 2;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mConfigSelector.setLayoutData(gd);
+ mConfigSelector.setOnChangeListener(new ConfigurationChangeListener());
+
+ // Folder name: [text]
+ String tooltip = "The folder where the file will be generated, relative to the project.";
+
+ Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridData gdSeparator = new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1);
+ gdSeparator.heightHint = 10;
+ separator.setLayoutData(gdSeparator);
+ Label folderLabel = new Label(composite, SWT.NONE);
+ folderLabel.setText("Folder:");
+ folderLabel.setToolTipText(tooltip);
+
+ mWsFolderPathTextField = new Text(composite, SWT.BORDER);
+ mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mWsFolderPathTextField.setToolTipText(tooltip);
+ mWsFolderPathTextField.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onWsFolderPathUpdated();
+ }
+ });
+
+ setControl(composite);
+
+ mConfigSelector.setConfiguration(mValues.configuration);
+ }
+
+ /**
+ * Callback called when the Folder text field is changed, either programmatically
+ * or by the user.
+ */
+ private void onWsFolderPathUpdated() {
+ if (mInternalWsFolderPathUpdate) {
+ return;
+ }
+
+ String wsFolderPath = mWsFolderPathTextField.getText();
+
+ // This is a custom path, we need to sanitize it.
+ // First it should start with "/res/". Then we need to make sure there are no
+ // relative paths, things like "../" or "./" or even "//".
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
+ if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
+ wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
+
+ mInternalWsFolderPathUpdate = true;
+ mWsFolderPathTextField.setText(wsFolderPath);
+ mInternalWsFolderPathUpdate = false;
+ }
+
+ mValues.folderPath = wsFolderPath;
+
+ if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
+
+ int pos = wsFolderPath.indexOf(AdtConstants.WS_SEP_CHAR);
+ if (pos >= 0) {
+ wsFolderPath = wsFolderPath.substring(0, pos);
+ }
+
+ String[] folderSegments = wsFolderPath.split(SdkConstants.RES_QUALIFIER_SEP);
+
+ if (folderSegments.length > 0) {
+ String folderName = folderSegments[0];
+
+ // update config selector
+ mInternalConfigSelectorUpdate = true;
+ mConfigSelector.setConfiguration(folderSegments);
+ mInternalConfigSelectorUpdate = false;
+
+ IWizardPage previous = ((NewXmlFileWizard) getWizard()).getPreviousPage(this);
+ if (previous instanceof NewXmlFileCreationPage) {
+ NewXmlFileCreationPage p = (NewXmlFileCreationPage) previous;
+ p.selectTypeFromFolder(folderName);
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ /**
+ * Callback called when the configuration has changed in the {@link ConfigurationSelector}.
+ */
+ private class ConfigurationChangeListener implements Runnable {
+ @Override
+ public void run() {
+ if (mInternalConfigSelectorUpdate) {
+ return;
+ }
+
+ resetFolderPath(true /*validate*/);
+ }
+ }
+
+ /**
+ * Reset the current Folder path based on the UI selection
+ * @param validate if true, force a call to {@link #validatePage()}.
+ */
+ private void resetFolderPath(boolean validate) {
+ TypeInfo type = mValues.type;
+ if (type != null) {
+ mConfigSelector.getConfiguration(mValues.configuration);
+ StringBuilder sb = new StringBuilder(RES_FOLDER_ABS);
+ sb.append(mValues.configuration.getFolderName(type.getResFolderType()));
+
+ mInternalWsFolderPathUpdate = true;
+ String newPath = sb.toString();
+ mValues.folderPath = newPath;
+ mWsFolderPathTextField.setText(newPath);
+ mInternalWsFolderPathUpdate = false;
+
+ if (validate) {
+ validatePage();
+ }
+ }
+ }
+
+ /**
+ * Returns the destination folder path relative to the project or an empty string.
+ *
+ * @return the currently edited folder
+ */
+ public String getWsFolderPath() {
+ return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$
+ }
+
+ /**
+ * Validates the fields, displays errors and warnings.
+ * Enables the finish button if there are no errors.
+ */
+ private void validatePage() {
+ String error = null;
+ String warning = null;
+
+ // -- validate folder configuration
+ if (error == null) {
+ ConfigurationState state = mConfigSelector.getState();
+ if (state == ConfigurationState.INVALID_CONFIG) {
+ ResourceQualifier qual = mConfigSelector.getInvalidQualifier();
+ if (qual != null) {
+ error =
+ String.format("The qualifier '%1$s' is invalid in the folder configuration.",
+ qual.getName());
+ }
+ } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) {
+ error = "The Region qualifier requires the Language qualifier.";
+ }
+ }
+
+ // -- validate generated path
+ if (error == null) {
+ String wsFolderPath = getWsFolderPath();
+ if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS);
+ }
+ }
+
+ // -- validate destination file doesn't exist
+ if (error == null) {
+ IFile file = mValues.getDestinationFile();
+ if (file != null && file.exists()) {
+ warning = "The destination file already exists";
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, IMessageProvider.ERROR);
+ } else if (warning != null) {
+ setMessage(warning, IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java
new file mode 100644
index 000000000..28fb8c032
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java
@@ -0,0 +1,1163 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
+
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
+import static com.android.SdkConstants.LINEAR_LAYOUT;
+import static com.android.SdkConstants.RES_QUALIFIER_SEP;
+import static com.android.SdkConstants.SCROLL_VIEW;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR;
+import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
+import com.android.resources.ResourceFolderType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create
+ * skeleton XML resources files for Android projects.
+ * <p/>
+ * This page is used to select the project, resource type and file name.
+ */
+class NewXmlFileCreationPage extends WizardPage {
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ // Ensure the initial focus is in the Name field; you usually don't need
+ // to edit the default text field (the project name)
+ if (visible && mFileNameTextField != null) {
+ mFileNameTextField.setFocus();
+ }
+
+ validatePage();
+ }
+
+ /**
+ * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.)
+ */
+ static class TypeInfo {
+ private final String mUiName;
+ private final ResourceFolderType mResFolderType;
+ private final String mTooltip;
+ private final Object mRootSeed;
+ private ArrayList<String> mRoots = new ArrayList<String>();
+ private final String mXmlns;
+ private final String mDefaultAttrs;
+ private final String mDefaultRoot;
+ private final int mTargetApiLevel;
+
+ public TypeInfo(String uiName,
+ String tooltip,
+ ResourceFolderType resFolderType,
+ Object rootSeed,
+ String defaultRoot,
+ String xmlns,
+ String defaultAttrs,
+ int targetApiLevel) {
+ mUiName = uiName;
+ mResFolderType = resFolderType;
+ mTooltip = tooltip;
+ mRootSeed = rootSeed;
+ mDefaultRoot = defaultRoot;
+ mXmlns = xmlns;
+ mDefaultAttrs = defaultAttrs;
+ mTargetApiLevel = targetApiLevel;
+ }
+
+ /** Returns the UI name for the resource type. Unique. Never null. */
+ String getUiName() {
+ return mUiName;
+ }
+
+ /** Returns the tooltip for the resource type. Can be null. */
+ String getTooltip() {
+ return mTooltip;
+ }
+
+ /**
+ * Returns the name of the {@link ResourceFolderType}.
+ * Never null but not necessarily unique,
+ * e.g. two types use {@link ResourceFolderType#XML}.
+ */
+ String getResFolderName() {
+ return mResFolderType.getName();
+ }
+
+ /**
+ * Returns the matching {@link ResourceFolderType}.
+ * Never null but not necessarily unique,
+ * e.g. two types use {@link ResourceFolderType#XML}.
+ */
+ ResourceFolderType getResFolderType() {
+ return mResFolderType;
+ }
+
+ /**
+ * Returns the seed used to fill the root element values.
+ * The seed might be either a String, a String array, an {@link ElementDescriptor},
+ * a {@link DocumentDescriptor} or null.
+ */
+ Object getRootSeed() {
+ return mRootSeed;
+ }
+
+ /**
+ * Returns the default root element that should be selected by default. Can be
+ * null.
+ *
+ * @param project the associated project, or null if not known
+ */
+ String getDefaultRoot(IProject project) {
+ return mDefaultRoot;
+ }
+
+ /**
+ * Returns the list of all possible root elements for the resource type.
+ * This can be an empty ArrayList but not null.
+ * <p/>
+ * TODO: the root list SHOULD depend on the currently selected project, to include
+ * custom classes.
+ */
+ ArrayList<String> getRoots() {
+ return mRoots;
+ }
+
+ /**
+ * If the generated resource XML file requires an "android" XMLNS, this should be set
+ * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated.
+ */
+ String getXmlns() {
+ return mXmlns;
+ }
+
+ /**
+ * When not null, this represent extra attributes that must be specified in the
+ * root element of the generated XML file. When null, no extra attributes are inserted.
+ *
+ * @param project the project to get the attributes for
+ * @param root the selected root element string, never null
+ */
+ String getDefaultAttrs(IProject project, String root) {
+ return mDefaultAttrs;
+ }
+
+ /**
+ * When not null, represents an extra string that should be written inside
+ * the element when constructed
+ *
+ * @param project the project to get the child content for
+ * @param root the chosen root element
+ * @return a string to be written inside the root element, or null if nothing
+ */
+ String getChild(IProject project, String root) {
+ return null;
+ }
+
+ /**
+ * The minimum API level required by the current SDK target to support this feature.
+ *
+ * @return the minimum API level
+ */
+ public int getTargetApiLevel() {
+ return mTargetApiLevel;
+ }
+ }
+
+ /**
+ * TypeInfo, information for each "type" of file that can be created.
+ */
+ private static final TypeInfo[] sTypes = {
+ new TypeInfo(
+ "Layout", // UI name
+ "An XML file that describes a screen layout.", // tooltip
+ ResourceFolderType.LAYOUT, // folder type
+ AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed
+ LINEAR_LAYOUT, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ "", // not used, see below
+ 1 // target API level
+ ) {
+
+ @Override
+ String getDefaultRoot(IProject project) {
+ // TODO: Use GridLayout by default for new SDKs
+ // (when we've ironed out all the usability issues)
+ //Sdk currentSdk = Sdk.getCurrent();
+ //if (project != null && currentSdk != null) {
+ // IAndroidTarget target = currentSdk.getTarget(project);
+ // // fill_parent was renamed match_parent in API level 8
+ // if (target != null && target.getVersion().getApiLevel() >= 13) {
+ // return GRID_LAYOUT;
+ // }
+ //}
+
+ return LINEAR_LAYOUT;
+ };
+
+ // The default attributes must be determined dynamically since whether
+ // we use match_parent or fill_parent depends on the API level of the
+ // project
+ @Override
+ String getDefaultAttrs(IProject project, String root) {
+ Sdk currentSdk = Sdk.getCurrent();
+ String fill = VALUE_FILL_PARENT;
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ // fill_parent was renamed match_parent in API level 8
+ if (target != null && target.getVersion().getApiLevel() >= 8) {
+ fill = VALUE_MATCH_PARENT;
+ }
+ }
+
+ // Only set "vertical" orientation of LinearLayouts by default;
+ // for GridLayouts for example we want to rely on the real default
+ // of the layout
+ String size = String.format(
+ "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$
+ + "android:layout_height=\"%2$s\"", //$NON-NLS-1$
+ fill, fill);
+ if (LINEAR_LAYOUT.equals(root)) {
+ return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$
+ } else {
+ return size;
+ }
+ }
+
+ @Override
+ String getChild(IProject project, String root) {
+ // Create vertical linear layouts inside new scroll views
+ if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) {
+ return " <LinearLayout " //$NON-NLS-1$
+ + getDefaultAttrs(project, root).replace('\n', ' ')
+ + " android:orientation=\"vertical\"" //$NON-NLS-1$
+ + "></LinearLayout>\n"; //$NON-NLS-1$
+ }
+ return null;
+ }
+ },
+ new TypeInfo("Values", // UI name
+ "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip
+ ResourceFolderType.VALUES, // folder type
+ SdkConstants.TAG_RESOURCES, // root seed
+ null, // default root
+ null, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Drawable", // UI name
+ "An XML file that describes a drawable.", // tooltip
+ ResourceFolderType.DRAWABLE, // folder type
+ AndroidTargetData.DESCRIPTOR_DRAWABLE, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Menu", // UI name
+ "An XML file that describes an menu.", // tooltip
+ ResourceFolderType.MENU, // folder type
+ SdkConstants.TAG_MENU, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Color List", // UI name
+ "An XML file that describes a color state list.", // tooltip
+ ResourceFolderType.COLOR, // folder type
+ AndroidTargetData.DESCRIPTOR_COLOR, // root seed
+ "selector", //$NON-NLS-1$ // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Property Animation", // UI name
+ "An XML file that describes a property animation", // tooltip
+ ResourceFolderType.ANIMATOR, // folder type
+ AndroidTargetData.DESCRIPTOR_ANIMATOR, // root seed
+ "set", //$NON-NLS-1$ // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 11 // target API level
+ ),
+ new TypeInfo("Tween Animation", // UI name
+ "An XML file that describes a tween animation.", // tooltip
+ ResourceFolderType.ANIM, // folder type
+ AndroidTargetData.DESCRIPTOR_ANIM, // root seed
+ "set", //$NON-NLS-1$ // default root
+ null, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("AppWidget Provider", // UI name
+ "An XML file that describes a widget provider.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 3 // target API level
+ ),
+ new TypeInfo("Preference", // UI name
+ "An XML file that describes preferences.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed
+ SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Searchable", // UI name
+ "An XML file that describes a searchable.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in
+ // this menu since it's not often used for creating XML files.
+ };
+
+ private NewXmlFileWizard.Values mValues;
+ private ProjectCombo mProjectButton;
+ private Text mFileNameTextField;
+ private Combo mTypeCombo;
+ private IStructuredSelection mInitialSelection;
+ private ResourceFolderType mInitialFolderType;
+ private boolean mInternalTypeUpdate;
+ private TargetChangeListener mSdkTargetChangeListener;
+ private Table mRootTable;
+ private TableViewer mRootTableViewer;
+
+ // --- UI creation ---
+
+ /**
+ * Constructs a new {@link NewXmlFileCreationPage}.
+ * <p/>
+ * Called by {@link NewXmlFileWizard#createMainPage}.
+ */
+ protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) {
+ super(pageName);
+ mValues = values;
+ setPageComplete(false);
+ }
+
+ public void setInitialSelection(IStructuredSelection initialSelection) {
+ mInitialSelection = initialSelection;
+ }
+
+ public void setInitialFolderType(ResourceFolderType initialType) {
+ mInitialFolderType = initialType;
+ }
+
+ /**
+ * Called by the parent Wizard to create the UI for this Wizard Page.
+ *
+ * {@inheritDoc}
+ *
+ * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused
+ public void createControl(Composite parent) {
+ // This UI is maintained with WindowBuilder.
+
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/));
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ // label before type radios
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setText("Resource Type:");
+
+ mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mTypeCombo.setToolTipText("What type of resource would you like to create?");
+ mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ if (mInitialFolderType != null) {
+ mTypeCombo.setEnabled(false);
+ }
+ mTypeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ TypeInfo type = getSelectedType();
+ if (type != null) {
+ onSelectType(type);
+ }
+ }
+ });
+
+ // separator
+ Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL);
+ gd2.horizontalAlignment = SWT.FILL;
+ gd2.horizontalSpan = 2;
+ separator.setLayoutData(gd2);
+
+ // Project: [button]
+ String tooltip = "The Android Project where the new resource file will be created.";
+ Label projectLabel = new Label(composite, SWT.NONE);
+ projectLabel.setText("Project:");
+ projectLabel.setToolTipText(tooltip);
+
+ ProjectChooserHelper helper =
+ new ProjectChooserHelper(getShell(), null /* filter */);
+
+ mProjectButton = new ProjectCombo(helper, composite, mValues.project);
+ mProjectButton.setToolTipText(tooltip);
+ mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IProject project = mProjectButton.getSelectedProject();
+ if (project != mValues.project) {
+ changeProject(project);
+ }
+ };
+ });
+
+ // Filename: [text]
+ Label fileLabel = new Label(composite, SWT.NONE);
+ fileLabel.setText("File:");
+ fileLabel.setToolTipText("The name of the resource file to create.");
+
+ mFileNameTextField = new Text(composite, SWT.BORDER);
+ mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFileNameTextField.setToolTipText(tooltip);
+ mFileNameTextField.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ mValues.name = mFileNameTextField.getText();
+ validatePage();
+ }
+ });
+
+ // separator
+ Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridData gd = new GridData(GridData.GRAB_HORIZONTAL);
+ gd.horizontalAlignment = SWT.FILL;
+ gd.horizontalSpan = 2;
+ rootSeparator.setLayoutData(gd);
+
+ // Root Element:
+ // [TableViewer]
+ Label rootLabel = new Label(composite, SWT.NONE);
+ rootLabel.setText("Root Element:");
+ new Label(composite, SWT.NONE);
+
+ mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION);
+ mRootTable = mRootTableViewer.getTable();
+ GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
+ tableGridData.heightHint = 200;
+ mRootTable.setLayoutData(tableGridData);
+
+ setControl(composite);
+
+ // Update state the first time
+ setErrorMessage(null);
+ setMessage(null);
+
+ initializeFromSelection(mInitialSelection);
+ updateAvailableTypes();
+ initializeFromFixedType();
+ initializeRootValues();
+ installTargetChangeListener();
+
+ initialSelectType();
+ validatePage();
+ }
+
+ private void initialSelectType() {
+ TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
+ int typeIndex = getTypeComboIndex(mValues.type);
+ if (typeIndex == -1) {
+ typeIndex = 0;
+ } else {
+ assert mValues.type == types[typeIndex];
+ }
+ mTypeCombo.select(typeIndex);
+ onSelectType(types[typeIndex]);
+ updateRootCombo(types[typeIndex]);
+ }
+
+ private void installTargetChangeListener() {
+ mSdkTargetChangeListener = new TargetChangeListener() {
+ @Override
+ public IProject getProject() {
+ return mValues.project;
+ }
+
+ @Override
+ public void reload() {
+ if (mValues.project != null) {
+ changeProject(mValues.project);
+ }
+ }
+ };
+
+ AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
+ }
+
+ @Override
+ public void dispose() {
+
+ if (mSdkTargetChangeListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
+ mSdkTargetChangeListener = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Returns the selected root element string, if any.
+ *
+ * @return The selected root element string or null.
+ */
+ public String getRootElement() {
+ int index = mRootTable.getSelectionIndex();
+ if (index >= 0) {
+ Object[] roots = (Object[]) mRootTableViewer.getInput();
+ return roots[index].toString();
+ }
+ return null;
+ }
+
+ /**
+ * Called by {@link NewXmlFileWizard} to initialize the page with the selection
+ * received by the wizard -- typically the current user workbench selection.
+ * <p/>
+ * Things we expect to find out from the selection:
+ * <ul>
+ * <li>The project name, valid if it's an android nature.</li>
+ * <li>The current folder, valid if it's a folder under /res</li>
+ * <li>An existing filename, in which case the user will be asked whether to override it.</li>
+ * </ul>
+ * <p/>
+ * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace
+ * resource path (where the resource path does not have to exist yet, such as res/anim/).
+ *
+ * @param selection The selection when the wizard was initiated.
+ */
+ private boolean initializeFromSelection(IStructuredSelection selection) {
+ if (selection == null) {
+ return false;
+ }
+
+ // Find the best match in the element list. In case there are multiple selected elements
+ // select the one that provides the most information and assign them a score,
+ // e.g. project=1 + folder=2 + file=4.
+ IProject targetProject = null;
+ String targetWsFolderPath = null;
+ String targetFileName = null;
+ int targetScore = 0;
+ for (Object element : selection.toList()) {
+ if (element instanceof IAdaptable) {
+ IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class);
+ IProject project = res != null ? res.getProject() : null;
+
+ // Is this an Android project?
+ try {
+ if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ continue;
+ }
+ } catch (CoreException e) {
+ // checking the nature failed, ignore this resource
+ continue;
+ }
+
+ int score = 1; // we have a valid project at least
+
+ IPath wsFolderPath = null;
+ String fileName = null;
+ assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no
+ if (res.getType() == IResource.FOLDER) {
+ wsFolderPath = res.getProjectRelativePath();
+ } else if (res.getType() == IResource.FILE) {
+ if (SdkUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) {
+ fileName = res.getName();
+ }
+ wsFolderPath = res.getParent().getProjectRelativePath();
+ }
+
+ // Disregard this folder selection if it doesn't point to /res/something
+ if (wsFolderPath != null &&
+ wsFolderPath.segmentCount() > 1 &&
+ SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) {
+ score += 2;
+ } else {
+ wsFolderPath = null;
+ fileName = null;
+ }
+
+ score += fileName != null ? 4 : 0;
+
+ if (score > targetScore) {
+ targetScore = score;
+ targetProject = project;
+ targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null;
+ targetFileName = fileName;
+ }
+ } else if (element instanceof Pair<?,?>) {
+ // Pair of Project/String
+ @SuppressWarnings("unchecked")
+ Pair<IProject,String> pair = (Pair<IProject,String>)element;
+ targetScore = 1;
+ targetProject = pair.getFirst();
+ targetWsFolderPath = pair.getSecond();
+ targetFileName = "";
+ }
+ }
+
+ if (targetProject == null) {
+ // Try to figure out the project from the active editor
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (window != null) {
+ IWorkbenchPage page = window.getActivePage();
+ if (page != null) {
+ IEditorPart activeEditor = page.getActiveEditor();
+ if (activeEditor instanceof AndroidXmlEditor) {
+ Object input = ((AndroidXmlEditor) activeEditor).getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ targetScore = 1;
+ IFile file = fileInput.getFile();
+ targetProject = file.getProject();
+ IPath path = file.getParent().getProjectRelativePath();
+ targetWsFolderPath = path != null ? path.toString() : null;
+ }
+ }
+ }
+ }
+ }
+
+ if (targetProject == null) {
+ // If we didn't find a default project based on the selection, check how many
+ // open Android projects we can find in the current workspace. If there's only
+ // one, we'll just select it by default.
+ IJavaProject[] projects = AdtUtils.getOpenAndroidProjects();
+ if (projects != null && projects.length == 1) {
+ targetScore = 1;
+ targetProject = projects[0].getProject();
+ }
+ }
+
+ // Now set the UI accordingly
+ if (targetScore > 0) {
+ mValues.project = targetProject;
+ mValues.folderPath = targetWsFolderPath;
+ mProjectButton.setSelectedProject(targetProject);
+ mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$
+
+ // If the current selection context corresponds to a specific file type,
+ // select it.
+ if (targetWsFolderPath != null) {
+ int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR);
+ if (pos >= 0) {
+ targetWsFolderPath = targetWsFolderPath.substring(pos + 1);
+ }
+ String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP);
+ if (folderSegments.length > 0) {
+ mValues.configuration = FolderConfiguration.getConfig(folderSegments);
+ String folderName = folderSegments[0];
+ selectTypeFromFolder(folderName);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private void initializeFromFixedType() {
+ if (mInitialFolderType != null) {
+ for (TypeInfo type : sTypes) {
+ if (type.getResFolderType() == mInitialFolderType) {
+ mValues.type = type;
+ updateFolderPath(type);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a folder name, such as "drawable", select the corresponding type in
+ * the dropdown.
+ */
+ void selectTypeFromFolder(String folderName) {
+ List<TypeInfo> matches = new ArrayList<TypeInfo>();
+ boolean selected = false;
+
+ TypeInfo selectedType = getSelectedType();
+ for (TypeInfo type : sTypes) {
+ if (type.getResFolderName().equals(folderName)) {
+ matches.add(type);
+ selected |= type == selectedType;
+ }
+ }
+
+ if (matches.size() == 1) {
+ // If there's only one match, select it if it's not already selected
+ if (!selected) {
+ selectType(matches.get(0));
+ }
+ } else if (matches.size() > 1) {
+ // There are multiple type candidates for this folder. This can happen
+ // for /res/xml for example. Check to see if one of them is currently
+ // selected. If yes, leave the selection unchanged. If not, deselect all type.
+ if (!selected) {
+ selectType(null);
+ }
+ } else {
+ // Nothing valid was selected.
+ selectType(null);
+ }
+ }
+
+ /**
+ * Initialize the root values of the type infos based on the current framework values.
+ */
+ private void initializeRootValues() {
+ IProject project = mValues.project;
+ for (TypeInfo type : sTypes) {
+ // Clear all the roots for this type
+ ArrayList<String> roots = type.getRoots();
+ if (roots.size() > 0) {
+ roots.clear();
+ }
+
+ // depending of the type of the seed, initialize the root in different ways
+ Object rootSeed = type.getRootSeed();
+
+ if (rootSeed instanceof String) {
+ // The seed is a single string, Add it as-is.
+ roots.add((String) rootSeed);
+ } else if (rootSeed instanceof String[]) {
+ // The seed is an array of strings. Add them as-is.
+ for (String value : (String[]) rootSeed) {
+ roots.add(value);
+ }
+ } else if (rootSeed instanceof Integer && project != null) {
+ // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_*
+ // In this case add all the children element descriptors defined, recursively,
+ // and avoid infinite recursion by keeping track of what has already been added.
+
+ // Note: if project is null, the root list will be empty since it has been
+ // cleared above.
+
+ // get the AndroidTargetData from the project
+ IAndroidTarget target = null;
+ AndroidTargetData data = null;
+
+ target = Sdk.getCurrent().getTarget(project);
+ if (target == null) {
+ // A project should have a target. The target can be missing if the project
+ // is an old project for which a target hasn't been affected or if the
+ // target no longer exists in this SDK. Simply log the error and dismiss.
+
+ AdtPlugin.log(IStatus.INFO,
+ "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$
+ project.getName());
+ continue;
+ } else {
+ data = Sdk.getCurrent().getTargetData(target);
+
+ if (data == null) {
+ // We should have both a target and its data.
+ // However if the wizard is invoked whilst the platform is still being
+ // loaded we can end up in a weird case where we have a target but it
+ // doesn't have any data yet.
+ // Lets log a warning and silently ignore this root.
+
+ AdtPlugin.log(IStatus.INFO,
+ "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$
+ target.getName(), project.getName());
+ continue;
+ }
+ }
+
+ IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
+ ElementDescriptor descriptor = provider.getDescriptor();
+ if (descriptor != null) {
+ HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>();
+ initRootElementDescriptor(roots, descriptor, visited);
+ }
+
+ // Sort alphabetically.
+ Collections.sort(roots);
+ }
+ }
+ }
+
+ /**
+ * Helper method to recursively insert all XML names for the given {@link ElementDescriptor}
+ * into the roots array list. Keeps track of visited nodes to avoid infinite recursion.
+ * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic
+ * and not a valid root element.
+ */
+ private void initRootElementDescriptor(ArrayList<String> roots,
+ ElementDescriptor desc, HashSet<ElementDescriptor> visited) {
+ if (!(desc instanceof DocumentDescriptor)) {
+ String xmlName = desc.getXmlName();
+ if (xmlName != null && xmlName.length() > 0) {
+ roots.add(xmlName);
+ }
+ }
+
+ visited.add(desc);
+
+ for (ElementDescriptor child : desc.getChildren()) {
+ if (!visited.contains(child)) {
+ initRootElementDescriptor(roots, child, visited);
+ }
+ }
+ }
+
+ /**
+ * Changes mProject to the given new project and update the UI accordingly.
+ * <p/>
+ * Note that this does not check if the new project is the same as the current one
+ * on purpose, which allows a project to be updated when its target has changed or
+ * when targets are loaded in the background.
+ */
+ private void changeProject(IProject newProject) {
+ mValues.project = newProject;
+
+ // enable types based on new API level
+ updateAvailableTypes();
+ initialSelectType();
+
+ // update the folder name based on API level
+ updateFolderPath(mValues.type);
+
+ // update the Type with the new descriptors.
+ initializeRootValues();
+
+ // update the combo
+ updateRootCombo(mValues.type);
+
+ validatePage();
+ }
+
+ private void onSelectType(TypeInfo type) {
+ // Do nothing if this is an internal modification or if the widget has been
+ // deselected.
+ if (mInternalTypeUpdate) {
+ return;
+ }
+
+ mValues.type = type;
+
+ if (type == null) {
+ return;
+ }
+
+ // update the combo
+ updateRootCombo(type);
+
+ // update the folder path
+ updateFolderPath(type);
+
+ validatePage();
+ }
+
+ /** Updates the selected type in the type dropdown control */
+ private void setSelectedType(TypeInfo type) {
+ TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
+ if (types != null) {
+ for (int i = 0, n = types.length; i < n; i++) {
+ if (types[i] == type) {
+ mTypeCombo.select(i);
+ break;
+ }
+ }
+ }
+ }
+
+ /** Returns the selected type in the type dropdown control */
+ private TypeInfo getSelectedType() {
+ int index = mTypeCombo.getSelectionIndex();
+ if (index != -1) {
+ TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
+ return types[index];
+ }
+
+ return null;
+ }
+
+ /** Returns the selected index in the type dropdown control */
+ private int getTypeComboIndex(TypeInfo type) {
+ TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData();
+ for (int i = 0, n = types.length; i < n; i++) {
+ if (type == types[i]) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /** Updates the folder path to reflect the given type */
+ private void updateFolderPath(TypeInfo type) {
+ String wsFolderPath = mValues.folderPath;
+ String newPath = null;
+ FolderConfiguration config = mValues.configuration;
+ ResourceQualifier qual = config.getInvalidQualifier();
+ if (qual == null) {
+ // The configuration is valid. Reformat the folder path using the canonical
+ // value from the configuration.
+ newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType());
+ } else {
+ // The configuration is invalid. We still update the path but this time
+ // do it manually on the string.
+ if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ wsFolderPath = wsFolderPath.replaceFirst(
+ "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
+ "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$
+ } else {
+ newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType());
+ }
+ }
+
+ if (newPath != null && !newPath.equals(wsFolderPath)) {
+ mValues.folderPath = newPath;
+ }
+ }
+
+ /**
+ * Helper method that fills the values of the "root element" combo box based
+ * on the currently selected type radio button. Also disables the combo is there's
+ * only one choice. Always select the first root element for the given type.
+ *
+ * @param type The currently selected {@link TypeInfo}, or null
+ */
+ private void updateRootCombo(TypeInfo type) {
+ IBaseLabelProvider labelProvider = new ColumnLabelProvider() {
+ @Override
+ public Image getImage(Object element) {
+ return IconFactory.getInstance().getIcon(element.toString());
+ }
+ };
+ mRootTableViewer.setContentProvider(new ArrayContentProvider());
+ mRootTableViewer.setLabelProvider(labelProvider);
+
+ if (type != null) {
+ // get the list of roots. The list can be empty but not null.
+ ArrayList<String> roots = type.getRoots();
+ mRootTableViewer.setInput(roots.toArray());
+
+ int index = 0; // default is to select the first one
+ String defaultRoot = type.getDefaultRoot(mValues.project);
+ if (defaultRoot != null) {
+ index = roots.indexOf(defaultRoot);
+ }
+ mRootTable.select(index < 0 ? 0 : index);
+ mRootTable.showSelection();
+ }
+ }
+
+ /**
+ * Helper method to select the current type in the type dropdown
+ *
+ * @param type The TypeInfo matching the radio button to selected or null to deselect them all.
+ */
+ private void selectType(TypeInfo type) {
+ mInternalTypeUpdate = true;
+ mValues.type = type;
+ if (type == null) {
+ if (mTypeCombo.getSelectionIndex() != -1) {
+ mTypeCombo.deselect(mTypeCombo.getSelectionIndex());
+ }
+ } else {
+ setSelectedType(type);
+ }
+ updateRootCombo(type);
+ mInternalTypeUpdate = false;
+ }
+
+ /**
+ * Add the available types in the type combobox, based on whether they are available
+ * for the current SDK.
+ * <p/>
+ * A type is available either if:
+ * - if mProject is null, API level 1 is considered valid
+ * - if mProject is !null, the project->target->API must be >= to the type's API level.
+ */
+ private void updateAvailableTypes() {
+ IProject project = mValues.project;
+ IAndroidTarget target = project != null ? Sdk.getCurrent().getTarget(project) : null;
+ int currentApiLevel = 1;
+ if (target != null) {
+ currentApiLevel = target.getVersion().getApiLevel();
+ }
+
+ List<String> items = new ArrayList<String>(sTypes.length);
+ List<TypeInfo> types = new ArrayList<TypeInfo>(sTypes.length);
+ for (int i = 0, n = sTypes.length; i < n; i++) {
+ TypeInfo type = sTypes[i];
+ if (type.getTargetApiLevel() <= currentApiLevel) {
+ items.add(type.getUiName());
+ types.add(type);
+ }
+ }
+ mTypeCombo.setItems(items.toArray(new String[items.size()]));
+ mTypeCombo.setData(types.toArray(new TypeInfo[types.size()]));
+ }
+
+ /**
+ * Validates the fields, displays errors and warnings.
+ * Enables the finish button if there are no errors.
+ */
+ private void validatePage() {
+ String error = null;
+ String warning = null;
+
+ // -- validate type
+ TypeInfo type = mValues.type;
+ if (error == null) {
+ if (type == null) {
+ error = "One of the types must be selected (e.g. layout, values, etc.)";
+ }
+ }
+
+ // -- validate project
+ if (mValues.project == null) {
+ error = "Please select an Android project.";
+ }
+
+ // -- validate type API level
+ if (error == null) {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mValues.project);
+ int currentApiLevel = 1;
+ if (target != null) {
+ currentApiLevel = target.getVersion().getApiLevel();
+ }
+
+ assert type != null;
+ if (type.getTargetApiLevel() > currentApiLevel) {
+ error = "The API level of the selected type (e.g. AppWidget, etc.) is not " +
+ "compatible with the API level of the project.";
+ }
+ }
+
+ // -- validate filename
+ if (error == null) {
+ String fileName = mValues.getFileName();
+ assert type != null;
+ ResourceFolderType folderType = type.getResFolderType();
+ error = ResourceNameValidator.create(true, folderType).isValid(fileName);
+ }
+
+ // -- validate destination file doesn't exist
+ if (error == null) {
+ IFile file = mValues.getDestinationFile();
+ if (file != null && file.exists()) {
+ warning = "The destination file already exists";
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, IMessageProvider.ERROR);
+ } else if (warning != null) {
+ setMessage(warning, IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ /**
+ * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if
+ * not found
+ *
+ * @param folderType the {@link ResourceFolderType} to look for
+ * @return the corresponding {@link TypeInfo}
+ */
+ static TypeInfo getTypeInfo(ResourceFolderType folderType) {
+ for (TypeInfo typeInfo : sTypes) {
+ if (typeInfo.getResFolderType() == folderType) {
+ return typeInfo;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java
new file mode 100644
index 000000000..16cd7b355
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
+
+import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
+import static com.android.SdkConstants.GRID_LAYOUT;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PartInitException;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * The "New Android XML File Wizard" provides the ability to create skeleton XML
+ * resources files for Android projects.
+ * <p/>
+ * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project,
+ * the resource folder, resource type and file name. It then creates the XML file.
+ */
+public class NewXmlFileWizard extends Wizard implements INewWizard {
+ /** The XML header to write at the top of the XML file */
+ public static final String XML_HEADER_LINE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$
+
+ private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$
+
+ protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$
+
+ private NewXmlFileCreationPage mMainPage;
+ private ChooseConfigurationPage mConfigPage;
+ private Values mValues;
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ setHelpAvailable(false); // TODO have help
+ setWindowTitle("New Android XML File");
+ setImageDescriptor();
+
+ mValues = new Values();
+ mMainPage = createMainPage(mValues);
+ mMainPage.setTitle("New Android XML File");
+ mMainPage.setDescription("Creates a new Android XML file.");
+ mMainPage.setInitialSelection(selection);
+
+ mConfigPage = new ChooseConfigurationPage(mValues);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ /**
+ * Creates the wizard page.
+ * <p/>
+ * Please do NOT override this method.
+ * <p/>
+ * This is protected so that it can be overridden by unit tests.
+ * However the contract of this class is private and NO ATTEMPT will be made
+ * to maintain compatibility between different versions of the plugin.
+ */
+ protected NewXmlFileCreationPage createMainPage(NewXmlFileWizard.Values values) {
+ return new NewXmlFileCreationPage(MAIN_PAGE_NAME, values);
+ }
+
+ // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
+ //
+ // The Wizard class implements most defaults and boilerplate code needed by
+ // IWizard
+
+ /**
+ * Adds pages to this wizard.
+ */
+ @Override
+ public void addPages() {
+ addPage(mMainPage);
+ addPage(mConfigPage);
+
+ }
+
+ /**
+ * Performs any actions appropriate in response to the user having pressed
+ * the Finish button, or refuse if finishing now is not permitted: here, it
+ * actually creates the workspace project and then switch to the Java
+ * perspective.
+ *
+ * @return True
+ */
+ @Override
+ public boolean performFinish() {
+ final Pair<IFile, IRegion> created = createXmlFile();
+ if (created == null) {
+ return false;
+ } else {
+ // Open the file
+ // This has to be delayed in order for focus handling to work correctly
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ IFile file = created.getFirst();
+ IRegion region = created.getSecond();
+ try {
+ IEditorPart editor = AdtPlugin.openFile(file, null,
+ false /*showEditorTab*/);
+ if (editor instanceof AndroidXmlEditor) {
+ final AndroidXmlEditor xmlEditor = (AndroidXmlEditor)editor;
+ if (!xmlEditor.hasMultiplePages()) {
+ xmlEditor.show(region.getOffset(), region.getLength(),
+ true /* showEditorTab */);
+ }
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$
+ file.getFullPath().toString());
+ }
+ }});
+
+ return true;
+ }
+ }
+
+ // -- Custom Methods --
+
+ private Pair<IFile, IRegion> createXmlFile() {
+ IFile file = mValues.getDestinationFile();
+ TypeInfo type = mValues.type;
+ if (type == null) {
+ // this is not expected to happen
+ String name = file.getFullPath().toString();
+ AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name); //$NON-NLS-1$
+ return null;
+ }
+ String xmlns = type.getXmlns();
+ String root = mMainPage.getRootElement();
+ if (root == null) {
+ // this is not expected to happen
+ AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$
+ file.toString());
+ return null;
+ }
+
+ String attrs = type.getDefaultAttrs(mValues.project, root);
+ String child = type.getChild(mValues.project, root);
+ return createXmlFile(file, xmlns, root, attrs, child, type.getResFolderType());
+ }
+
+ /** Creates a new file using the given root element, namespace and root attributes */
+ private static Pair<IFile, IRegion> createXmlFile(IFile file, String xmlns,
+ String root, String rootAttributes, String child, ResourceFolderType folderType) {
+ String name = file.getFullPath().toString();
+ boolean need_delete = false;
+
+ if (file.exists()) {
+ if (!AdtPlugin.displayPrompt("New Android XML File",
+ String.format("Do you want to overwrite the file %1$s ?", name))) {
+ // abort if user selects cancel.
+ return null;
+ }
+ need_delete = true;
+ } else {
+ AdtUtils.createWsParentDirectory(file.getParent());
+ }
+
+ StringBuilder sb = new StringBuilder(XML_HEADER_LINE);
+
+ if (folderType == ResourceFolderType.LAYOUT && root.equals(GRID_LAYOUT)) {
+ IProject project = file.getParent().getProject();
+ int minSdk = ManifestInfo.get(project).getMinSdkVersion();
+ if (minSdk < 14) {
+ root = SupportLibraryHelper.getTagFor(project, FQCN_GRID_LAYOUT);
+ if (root.equals(FQCN_GRID_LAYOUT)) {
+ root = GRID_LAYOUT;
+ }
+ }
+ }
+
+ sb.append('<').append(root);
+ if (xmlns != null) {
+ sb.append('\n').append(" xmlns:android=\"").append(xmlns).append('"'); //$NON-NLS-1$
+ }
+
+ if (rootAttributes != null) {
+ sb.append("\n "); //$NON-NLS-1$
+ sb.append(rootAttributes.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ sb.append(">\n"); //$NON-NLS-1$
+
+ if (child != null) {
+ sb.append(child);
+ }
+
+ boolean autoFormat = AdtPrefs.getPrefs().getUseCustomXmlFormatter();
+
+ // Insert an indented caret. Since the markup here will be reformatted, we need to
+ // insert text tokens that the formatter will preserve, which we can then turn back
+ // into indentation and a caret offset:
+ final String indentToken = "${indent}"; //$NON-NLS-1$
+ final String caretToken = "${caret}"; //$NON-NLS-1$
+ sb.append(indentToken);
+ sb.append(caretToken);
+ if (!autoFormat) {
+ sb.append('\n');
+ }
+
+ sb.append("</").append(root).append(">\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
+ String fileContents;
+ if (!autoFormat) {
+ fileContents = sb.toString();
+ } else {
+ XmlFormatStyle style = EclipseXmlPrettyPrinter.getForFolderType(folderType);
+ fileContents = EclipseXmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs,
+ style, null /*lineSeparator*/);
+ }
+
+ // Remove marker tokens and replace them with whitespace
+ fileContents = fileContents.replace(indentToken, formatPrefs.getOneIndentUnit());
+ int caretOffset = fileContents.indexOf(caretToken);
+ if (caretOffset != -1) {
+ fileContents = fileContents.replace(caretToken, ""); //$NON-NLS-1$
+ }
+
+ String error = null;
+ try {
+ byte[] buf = fileContents.getBytes("UTF8"); //$NON-NLS-1$
+ InputStream stream = new ByteArrayInputStream(buf);
+ if (need_delete) {
+ file.delete(IResource.KEEP_HISTORY | IResource.FORCE, null /*monitor*/);
+ }
+ file.create(stream, true /*force*/, null /*progress*/);
+ IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null;
+
+ // If you introduced a new locale, or new screen variations etc, ensure that
+ // the list of render previews is updated if necessary
+ if (file.getParent().getName().indexOf('-') != -1
+ && (folderType == ResourceFolderType.LAYOUT
+ || folderType == ResourceFolderType.VALUES)) {
+ RenderPreviewManager.bumpRevision();
+ }
+
+ return Pair.of(file, region);
+ } catch (UnsupportedEncodingException e) {
+ error = e.getMessage();
+ } catch (CoreException e) {
+ error = e.getMessage();
+ }
+
+ error = String.format("Failed to generate %1$s: %2$s", name, error);
+ AdtPlugin.displayError("New Android XML File", error);
+ return null;
+ }
+
+ /**
+ * Returns true if the New XML Wizard can create new files of the given
+ * {@link ResourceFolderType}
+ *
+ * @param folderType the folder type to create a file for
+ * @return true if this wizard can create new files for the given folder type
+ */
+ public static boolean canCreateXmlFile(ResourceFolderType folderType) {
+ TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType);
+ return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null ||
+ typeInfo.getRootSeed() instanceof String);
+ }
+
+ /**
+ * Creates a new XML file using the template according to the given folder type
+ *
+ * @param project the project to create the file in
+ * @param file the file to be created
+ * @param folderType the type of folder to look up a template for
+ * @return the created file
+ */
+ public static Pair<IFile, IRegion> createXmlFile(IProject project, IFile file,
+ ResourceFolderType folderType) {
+ TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType);
+ String xmlns = type.getXmlns();
+ String root = type.getDefaultRoot(project);
+ if (root == null) {
+ root = type.getRootSeed().toString();
+ }
+ String attrs = type.getDefaultAttrs(project, root);
+ return createXmlFile(file, xmlns, root, attrs, null, folderType);
+ }
+
+ /**
+ * Returns an image descriptor for the wizard logo.
+ */
+ private void setImageDescriptor() {
+ ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+ }
+
+ /**
+ * Specific New XML File wizard tied to the {@link ResourceFolderType#LAYOUT} type
+ */
+ public static class NewLayoutWizard extends NewXmlFileWizard {
+ /** Creates a new {@link NewLayoutWizard} */
+ public NewLayoutWizard() {
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+ setWindowTitle("New Android Layout XML File");
+ super.mMainPage.setTitle("New Android Layout XML File");
+ super.mMainPage.setDescription("Creates a new Android Layout XML file.");
+ super.mMainPage.setInitialFolderType(ResourceFolderType.LAYOUT);
+ }
+ }
+
+ /**
+ * Specific New XML File wizard tied to the {@link ResourceFolderType#VALUES} type
+ */
+ public static class NewValuesWizard extends NewXmlFileWizard {
+ /** Creates a new {@link NewValuesWizard} */
+ public NewValuesWizard() {
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+ setWindowTitle("New Android Values XML File");
+ super.mMainPage.setTitle("New Android Values XML File");
+ super.mMainPage.setDescription("Creates a new Android Values XML file.");
+ super.mMainPage.setInitialFolderType(ResourceFolderType.VALUES);
+ }
+ }
+
+ /** Value object which holds the current state of the wizard pages */
+ public static class Values {
+ /** The currently selected project, or null */
+ public IProject project;
+ /** The root name of the XML file to create, or null */
+ public String name;
+ /** The type of XML file to create */
+ public TypeInfo type;
+ /** The path within the project to create the new file in */
+ public String folderPath;
+ /** The currently chosen configuration */
+ public FolderConfiguration configuration = new FolderConfiguration();
+
+ /**
+ * Returns the destination filename or an empty string.
+ *
+ * @return the filename, never null.
+ */
+ public String getFileName() {
+ String fileName;
+ if (name == null) {
+ fileName = ""; //$NON-NLS-1$
+ } else {
+ fileName = name.trim();
+ if (fileName.length() > 0 && fileName.indexOf('.') == -1) {
+ fileName = fileName + SdkConstants.DOT_XML;
+ }
+ }
+
+ return fileName;
+ }
+
+ /**
+ * Returns a {@link IFile} for the destination file.
+ * <p/>
+ * Returns null if the project, filename or folder are invalid and the
+ * destination file cannot be determined.
+ * <p/>
+ * The {@link IFile} is a resource. There might or might not be an
+ * actual real file.
+ *
+ * @return an {@link IFile} for the destination file
+ */
+ public IFile getDestinationFile() {
+ String fileName = getFileName();
+ if (project != null && folderPath != null && folderPath.length() > 0
+ && fileName.length() > 0) {
+ IPath dest = new Path(folderPath).append(fileName);
+ IFile file = project.getFile(dest);
+ return file;
+ }
+ return null;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java
new file mode 100644
index 000000000..ba4aedc8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_ACTIVITIES;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_OTHER;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LAUNCHER;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+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.Label;
+import org.eclipse.swt.widgets.List;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+class ActivityPage extends WizardPage implements SelectionListener {
+ private final NewProjectWizardState mValues;
+ private List mList;
+ private Button mCreateToggle;
+ private java.util.List<File> mTemplates;
+
+ private boolean mIgnore;
+ private boolean mShown;
+ private ImageControl mPreview;
+ private Image mPreviewImage;
+ private boolean mDisposePreviewImage;
+ private Label mHeading;
+ private Label mDescription;
+ private boolean mOnlyActivities;
+ private boolean mAskCreate;
+ private boolean mLauncherActivitiesOnly;
+
+ /**
+ * Create the wizard.
+ */
+ ActivityPage(NewProjectWizardState values, boolean onlyActivities, boolean askCreate) {
+ super("activityPage"); //$NON-NLS-1$
+ mValues = values;
+ mOnlyActivities = onlyActivities;
+ mAskCreate = askCreate;
+
+ if (onlyActivities) {
+ setTitle("Create Activity");
+ } else {
+ setTitle("Create Android Object");
+ }
+ if (onlyActivities && askCreate) {
+ setDescription(
+ "Select whether to create an activity, and if so, what kind of activity.");
+ } else {
+ setDescription("Select which template to use");
+ }
+ }
+
+ /** Sets whether the activity page should only offer launcher activities */
+ void setLauncherActivitiesOnly(boolean launcherActivitiesOnly) {
+ mLauncherActivitiesOnly = launcherActivitiesOnly;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ private void onEnter() {
+ Composite container = (Composite) getControl();
+ container.setLayout(new GridLayout(3, false));
+
+ if (mAskCreate) {
+ mCreateToggle = new Button(container, SWT.CHECK);
+ mCreateToggle.setSelection(true);
+ mCreateToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1));
+ mCreateToggle.setText("Create Activity");
+ mCreateToggle.addSelectionListener(this);
+ }
+
+ mList = new List(container, SWT.BORDER | SWT.V_SCROLL);
+ mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+
+
+ TemplateManager manager = mValues.template.getManager();
+ java.util.List<File> templates = manager.getTemplates(CATEGORY_ACTIVITIES);
+
+ if (!mOnlyActivities) {
+ templates.addAll(manager.getTemplates(CATEGORY_OTHER));
+ }
+ java.util.List<String> names = new ArrayList<String>(templates.size());
+ File current = mValues.activityValues.getTemplateLocation();
+ mTemplates = Lists.newArrayListWithExpectedSize(templates.size());
+ int index = -1;
+ for (int i = 0, n = templates.size(); i < n; i++) {
+ File template = templates.get(i);
+ TemplateMetadata metadata = manager.getTemplate(template);
+ if (metadata == null) {
+ continue;
+ }
+ if (mLauncherActivitiesOnly) {
+ Parameter parameter = metadata.getParameter(IS_LAUNCHER);
+ if (parameter == null) {
+ continue;
+ }
+ }
+ mTemplates.add(template);
+ names.add(metadata.getTitle());
+ if (template.equals(current)) {
+ index = names.size();
+ }
+ }
+ String[] items = names.toArray(new String[names.size()]);
+ mList.setItems(items);
+ if (index == -1 && !mTemplates.isEmpty()) {
+ mValues.activityValues.setTemplateLocation(mTemplates.get(0));
+ index = 0;
+ }
+ if (index >= 0) {
+ mList.setSelection(index);
+ mList.addSelectionListener(this);
+ }
+
+ // Preview
+ mPreview = new ImageControl(container, SWT.NONE, null);
+ mPreview.setDisposeImage(false); // Handled manually in this class
+ GridData gd_mImage = new GridData(SWT.CENTER, SWT.CENTER, false, false, 1, 1);
+ gd_mImage.widthHint = PREVIEW_WIDTH + 2 * PREVIEW_PADDING;
+ mPreview.setLayoutData(gd_mImage);
+ new Label(container, SWT.NONE);
+
+ mHeading = new Label(container, SWT.NONE);
+ mHeading.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ new Label(container, SWT.NONE);
+
+ mDescription = new Label(container, SWT.WRAP);
+ mDescription.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1));
+
+ Font font = JFaceResources.getFontRegistry().getBold(JFaceResources.BANNER_FONT);
+ if (font != null) {
+ mHeading.setFont(font);
+ }
+
+ updatePreview();
+ }
+
+ private void updatePreview() {
+ Image oldImage = mPreviewImage;
+ boolean dispose = mDisposePreviewImage;
+ mPreviewImage = null;
+
+ String title = "";
+ String description = "";
+ TemplateHandler handler = mValues.activityValues.getTemplateHandler();
+ TemplateMetadata template = handler.getTemplate();
+ if (template != null) {
+ String thumb = template.getThumbnailPath();
+ if (thumb != null && !thumb.isEmpty()) {
+ File file = new File(mValues.activityValues.getTemplateLocation(),
+ thumb.replace('/', File.separatorChar));
+ if (file != null) {
+ try {
+ byte[] bytes = Files.toByteArray(file);
+ ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+ mPreviewImage = new Image(getControl().getDisplay(), input);
+ mDisposePreviewImage = true;
+ input.close();
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ } else {
+ // Fallback icon
+ mDisposePreviewImage = false;
+ mPreviewImage = TemplateMetadata.getDefaultTemplateIcon();
+ }
+ title = template.getTitle();
+ description = template.getDescription();
+ }
+
+ mHeading.setText(title);
+ mDescription.setText(description);
+ mPreview.setImage(mPreviewImage);
+ mPreview.fitToWidth(PREVIEW_WIDTH);
+
+ if (oldImage != null && dispose) {
+ oldImage.dispose();
+ }
+
+ Composite parent = (Composite) getControl();
+ parent.layout(true, true);
+ parent.redraw();
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mPreviewImage != null && mDisposePreviewImage) {
+ mDisposePreviewImage = false;
+ mPreviewImage.dispose();
+ mPreviewImage = null;
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible && !mShown) {
+ onEnter();
+ }
+
+ super.setVisible(visible);
+
+ if (visible) {
+ mShown = true;
+ if (mAskCreate) {
+ try {
+ mIgnore = true;
+ mCreateToggle.setSelection(mValues.createActivity);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+
+ private void validatePage() {
+ IStatus status = null;
+
+ if (mValues.createActivity) {
+ if (mList.getSelectionCount() < 1) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Select an activity type");
+ } else {
+ TemplateHandler templateHandler = mValues.activityValues.getTemplateHandler();
+ status = templateHandler.validateTemplate(mValues.minSdkLevel,
+ mValues.getBuildApi());
+ }
+ }
+
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ // Ensure that the Finish button isn't enabled until
+ // the user has reached and completed this page
+ if (!mShown && mValues.createActivity) {
+ return false;
+ }
+
+ return super.isPageComplete();
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mCreateToggle) {
+ mValues.createActivity = mCreateToggle.getSelection();
+ mList.setEnabled(mValues.createActivity);
+ } else if (source == mList) {
+ int index = mList.getSelectionIndex();
+ if (index >= 0 && index < mTemplates.size()) {
+ File template = mTemplates.get(index);
+ mValues.activityValues.setTemplateLocation(template);
+ updatePreview();
+ }
+ }
+
+ validatePage();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java
new file mode 100644
index 000000000..3b41c36c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.google.common.io.Closeables;
+import com.google.common.io.Files;
+import com.google.common.io.InputSupplier;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.resource.ResourceChange;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
+/** Change which lazily copies a file */
+public class CreateFileChange extends ResourceChange {
+ private String mName;
+ private final IPath mPath;
+ private final File mSource;
+
+ CreateFileChange(@NonNull String name, @NonNull IPath workspacePath, File source) {
+ mName = name;
+ mPath = workspacePath;
+ mSource = source;
+ }
+
+ @Override
+ protected IResource getModifiedResource() {
+ return ResourcesPlugin.getWorkspace().getRoot().getFile(mPath);
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public RefactoringStatus isValid(IProgressMonitor pm)
+ throws CoreException, OperationCanceledException {
+ RefactoringStatus result = new RefactoringStatus();
+ IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(mPath);
+ URI location = file.getLocationURI();
+ if (location == null) {
+ result.addFatalError("Unknown location " + file.getFullPath().toString());
+ return result;
+ }
+ return result;
+ }
+
+ @SuppressWarnings("resource") // Eclipse doesn't know about Guava's Closeables.closeQuietly
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ InputSupplier<FileInputStream> supplier = Files.newInputStreamSupplier(mSource);
+ InputStream is = null;
+ try {
+ pm.beginTask("Creating file", 3);
+ IFile file = (IFile) getModifiedResource();
+
+ IContainer parent = file.getParent();
+ if (parent != null && !parent.exists()) {
+ IFolder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(
+ parent.getFullPath());
+ AdtUtils.ensureExists(folder);
+ }
+
+ is = supplier.getInput();
+ file.create(is, false, new SubProgressMonitor(pm, 1));
+ pm.worked(1);
+ } catch (Exception ioe) {
+ AdtPlugin.log(ioe, null);
+ } finally {
+ Closeables.closeQuietly(is);
+ pm.done();
+ }
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java
new file mode 100644
index 000000000..fbd50e986
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.LAYOUT_NAME_PREFIX;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to convert an Activity class name into
+ * a suitable layout name.
+ */
+public class FmActivityToLayoutMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+
+ String activityName = args.get(0).toString();
+
+ if (activityName.isEmpty()) {
+ return new SimpleScalar("");
+ }
+
+ // Strip off the end portion of the activity name. The user might be typing
+ // the activity name such that only a portion has been entered so far (e.g.
+ // "MainActivi") and we want to chop off that portion too such that we don't
+ // offer a layout name partially containing the activity suffix (e.g. "main_activi").
+ int suffixStart = activityName.lastIndexOf(ACTIVITY_NAME_SUFFIX.charAt(0));
+ if (suffixStart != -1 && activityName.regionMatches(suffixStart, ACTIVITY_NAME_SUFFIX, 0,
+ activityName.length() - suffixStart)) {
+ activityName = activityName.substring(0, suffixStart);
+ }
+ assert !activityName.endsWith(ACTIVITY_NAME_SUFFIX) : activityName;
+
+ // Convert CamelCase convention used in activity class names to underlined convention
+ // used in layout name:
+ String name = LAYOUT_NAME_PREFIX + AdtUtils.camelCaseToUnderlines(activityName);
+
+ return new SimpleScalar(name);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java
new file mode 100644
index 000000000..b85576577
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to convert an underscore name into a CamelCase name.
+ */
+public class FmCamelCaseToUnderscoreMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ return new SimpleScalar(AdtUtils.camelCaseToUnderlines(args.get(0).toString()));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java
new file mode 100644
index 000000000..366de9afa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Similar to {@link FmCamelCaseToUnderscoreMethod}, but strips off common class
+ * suffixes such as "Activity", "Fragment", etc.
+ */
+public class FmClassNameToResourceMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+
+ String name = args.get(0).toString();
+
+ if (name.isEmpty()) {
+ return new SimpleScalar("");
+ }
+
+ name = stripSuffix(name, ACTIVITY_NAME_SUFFIX);
+ name = stripSuffix(name, "Fragment"); //$NON-NLS-1$
+ name = stripSuffix(name, "Service"); //$NON-NLS-1$
+ name = stripSuffix(name, "Provider"); //$NON-NLS-1$
+
+ return new SimpleScalar(AdtUtils.camelCaseToUnderlines(name));
+ }
+
+ // Strip off the end portion of the activity name. The user might be typing
+ // the activity name such that only a portion has been entered so far (e.g.
+ // "MainActivi") and we want to chop off that portion too such that we don't
+ private static String stripSuffix(String name, String suffix) {
+ int suffixStart = name.lastIndexOf(suffix.charAt(0));
+ if (suffixStart != -1 && name.regionMatches(suffixStart, suffix, 0,
+ name.length() - suffixStart)) {
+ name = name.substring(0, suffixStart);
+ }
+ assert !name.endsWith(suffix) : name;
+
+ return name;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java
new file mode 100644
index 000000000..21f33b8d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.utils.XmlUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to escape a string such that it can be used
+ * as an XML attribute (escaping ', ", & and <).
+ */
+public class FmEscapeXmlAttributeMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(XmlUtils.toXmlAttributeValue(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
new file mode 100644
index 000000000..2255653a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.ide.common.res2.ValueXmlHelper;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to escape a string such that it can be placed
+ * as text in a string resource file.
+ * This is similar to {@link FmEscapeXmlTextMethod}, but in addition to escaping
+ * &lt; and &amp; it also escapes characters such as quotes necessary for Android
+ *{@code <string>} elements.
+ */
+public class FmEscapeXmlStringMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(ValueXmlHelper.escapeResourceString(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java
new file mode 100644
index 000000000..55a4bc8ab
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.utils.XmlUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to escape a string such that it can be used
+ * as XML text (escaping < and &, but not ' and " etc).
+ */
+public class FmEscapeXmlTextMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(XmlUtils.toXmlTextValue(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java
new file mode 100644
index 000000000..09fa81c57
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to extract letters from a string; this will remove
+ * any whitespace, punctuation and digits.
+ */
+public class FmExtractLettersMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ StringBuilder sb = new StringBuilder(string.length());
+ for (int i = 0, n = string.length(); i < n; i++) {
+ char c = string.charAt(i);
+ if (Character.isLetter(c)) {
+ sb.append(c);
+ }
+ }
+ return new SimpleScalar(sb.toString());
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java
new file mode 100644
index 000000000..6514959f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.AdtUtils.extractClassName;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.LAYOUT_NAME_PREFIX;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to convert a layout name into an appropriate
+ * Activity class.
+ */
+public class FmLayoutToActivityMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+
+ String name = args.get(0).toString();
+
+ // Strip off the beginning portion of the layout name. The user might be typing
+ // the activity name such that only a portion has been entered so far (e.g.
+ // "MainActivi") and we want to chop off that portion too such that we don't
+ // offer a layout name partially containing the activity suffix (e.g. "main_activi").
+ if (name.startsWith(LAYOUT_NAME_PREFIX)) {
+ name = name.substring(LAYOUT_NAME_PREFIX.length());
+ }
+
+ name = AdtUtils.underlinesToCamelCase(name);
+ String className = extractClassName(name);
+ if (className == null) {
+ className = "My";
+ }
+ String activityName = className + ACTIVITY_NAME_SUFFIX;
+
+ return new SimpleScalar(activityName);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java
new file mode 100644
index 000000000..60a6531e6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to convert a package name (foo.bar) into
+ * a slashed path (foo/bar)
+ */
+public class FmSlashedPackageNameMethod implements TemplateMethodModel {
+
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+
+ return new SimpleScalar(args.get(0).toString().replace('.', '/'));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java
new file mode 100644
index 000000000..26d4fadb4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+import java.util.List;
+
+/**
+ * Method invoked by FreeMarker to convert a CamelCase word into
+ * underscore_names.
+ */
+public class FmUnderscoreToCamelCaseMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ return new SimpleScalar(AdtUtils.underlinesToCamelCase(args.get(0).toString()));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java
new file mode 100644
index 000000000..d806e7970
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.wizard.IWizard;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+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.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.browser.IWebBrowser;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+
+class InstallDependencyPage extends WizardPage implements SelectionListener {
+ /**
+ * The compatibility library. This is the only library the templates
+ * currently support. The appearance of any other dependency in this
+ * template will be flagged as a validation error (and the user encouraged
+ * to upgrade to a newer ADT
+ */
+ static final String SUPPORT_LIBRARY_NAME = "android-support-v4"; //$NON-NLS-1$
+
+ /** URL containing more info */
+ private static final String URL =
+ "http://developer.android.com/tools/extras/support-library.html"; //$NON-NLS-1$
+
+ private Button mCheckButton;
+ private Button mInstallButton;
+ private Link mLink;
+ private TemplateMetadata mTemplate;
+
+ InstallDependencyPage() {
+ super("dependency"); //$NON-NLS-1$
+ setTitle("Install Dependencies");
+ }
+
+ void setTemplate(TemplateMetadata template) {
+ if (template != mTemplate) {
+ mTemplate = template;
+ if (getControl() != null) {
+ validatePage();
+ }
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ updateVersionLabels();
+ validatePage();
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(2, false));
+ // Remaining contents are created lazily, since this page is always added to
+ // the page list, but typically not shown
+
+ Label dependLabel = new Label(container, SWT.WRAP);
+ GridData gd_dependLabel = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
+ gd_dependLabel.widthHint = NewTemplatePage.WIZARD_PAGE_WIDTH - 50;
+ dependLabel.setLayoutData(gd_dependLabel);
+ dependLabel.setText("This template depends on the Android Support library, which is " +
+ "either not installed, or the template depends on a more recent version than " +
+ "the one you have installed.");
+
+ mLink = new Link(container, SWT.NONE);
+ mLink.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mLink.setText("<a href=\"" + URL + "\">" + URL + "</a>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ mLink.addSelectionListener(this);
+
+ Label lblNewLabel_1 = new Label(container, SWT.NONE);
+ lblNewLabel_1.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+
+ requiredLabel = new Label(container, SWT.NONE);
+ requiredLabel.setText("Required version:");
+
+ mRequiredVersion = new Label(container, SWT.NONE);
+ mRequiredVersion.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+
+ installedLabel = new Label(container, SWT.NONE);
+ installedLabel.setText("Installed version:");
+
+ mInstalledVersion = new Label(container, SWT.NONE);
+ mInstalledVersion.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+
+ Label lblNewLabel = new Label(container, SWT.NONE);
+ lblNewLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+
+ Label descLabel = new Label(container, SWT.WRAP);
+ GridData gd_descLabel = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
+ gd_descLabel.widthHint = 550;
+ descLabel.setLayoutData(gd_descLabel);
+ descLabel.setText(
+ "You can install or upgrade it by clicking the Install button below, or " +
+ "alternatively, you can install it outside of Eclipse with the SDK Manager, " +
+ "then click on \"Check Again\" to proceed.");
+
+ mInstallButton = new Button(container, SWT.NONE);
+ mInstallButton.setText("Install/Upgrade");
+ mInstallButton.addSelectionListener(this);
+
+ mCheckButton = new Button(container, SWT.NONE);
+ mCheckButton.setText("Check Again");
+ mCheckButton.addSelectionListener(this);
+
+ mInstallButton.setFocus();
+ }
+
+ private void showNextPage() {
+ validatePage();
+ if (isPageComplete()) {
+ // Finish button will be enabled now
+ mInstallButton.setEnabled(false);
+ mCheckButton.setEnabled(false);
+
+ IWizard wizard = getWizard();
+ IWizardPage next = wizard.getNextPage(this);
+ if (next != null) {
+ wizard.getContainer().showPage(next);
+ }
+ }
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ if (mTemplate == null) {
+ return true;
+ }
+
+ return super.isPageComplete() && isInstalled();
+ }
+
+ private boolean isInstalled() {
+ return isInstalled(mTemplate.getDependencies());
+ }
+
+ static String sCachedName;
+ static int sCachedVersion;
+ private Label requiredLabel;
+ private Label installedLabel;
+ private Label mRequiredVersion;
+ private Label mInstalledVersion;
+
+ public static boolean isInstalled(List<Pair<String, Integer>> dependencies) {
+ for (Pair<String, Integer> dependency : dependencies) {
+ String name = dependency.getFirst();
+ int required = dependency.getSecond();
+
+ int installed = -1;
+ if (SUPPORT_LIBRARY_NAME.equals(name)) {
+ installed = getInstalledSupportLibVersion();
+ }
+
+ if (installed == -1) {
+ return false;
+ }
+ if (required > installed) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int getInstalledSupportLibVersion() {
+ if (SUPPORT_LIBRARY_NAME.equals(sCachedName)) {
+ return sCachedVersion;
+ } else {
+ int version = AddSupportJarAction.getInstalledRevision();
+ sCachedName = SUPPORT_LIBRARY_NAME;
+ sCachedVersion = version;
+ return version;
+ }
+ }
+
+ private void updateVersionLabels() {
+ int version = getInstalledSupportLibVersion();
+ if (version == -1) {
+ mInstalledVersion.setText("Not installed");
+ } else {
+ mInstalledVersion.setText(Integer.toString(version));
+ }
+
+ if (mTemplate != null) {
+ for (Pair<String, Integer> dependency : mTemplate.getDependencies()) {
+ String name = dependency.getFirst();
+ if (name.equals(SUPPORT_LIBRARY_NAME)) {
+ int required = dependency.getSecond();
+ mRequiredVersion.setText(Integer.toString(required));
+ break;
+ }
+ }
+ }
+ }
+
+ private void validatePage() {
+ if (mTemplate == null) {
+ return;
+ }
+
+ IStatus status = null;
+
+ List<Pair<String, Integer>> dependencies = mTemplate.getDependencies();
+ if (dependencies.size() > 1 || dependencies.size() == 1
+ && !dependencies.get(0).getFirst().equals(SUPPORT_LIBRARY_NAME)) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "Unsupported template dependency: Upgrade your Android Eclipse plugin");
+ }
+
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+ if (source == mCheckButton) {
+ sCachedName = null;
+ if (isInstalled()) {
+ showNextPage();
+ }
+ updateVersionLabels();
+ } else if (source == mInstallButton) {
+ sCachedName = null;
+ for (Pair<String, Integer> dependency : mTemplate.getDependencies()) {
+ String name = dependency.getFirst();
+ if (SUPPORT_LIBRARY_NAME.equals(name)) {
+ int version = dependency.getSecond();
+ File installed = AddSupportJarAction.installSupport(version);
+ if (installed != null) {
+ showNextPage();
+ }
+ updateVersionLabels();
+ }
+ }
+ } else if (source == mLink) {
+ try {
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
+ browser.openURL(new URL(URL));
+ } catch (Exception ex) {
+ String message = String.format("Could not open browser. Vist\n%1$s\ninstead.",
+ URL);
+ MessageDialog.openError(getShell(), "Browser Error", message);
+ }
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java
new file mode 100644
index 000000000..b33d65bb7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API_LEVEL;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_TARGET_API;
+import static org.eclipse.core.resources.IResource.DEPTH_INFINITE;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ui.IWorkbench;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Wizard for creating new activities. This is a hybrid between a New Project
+ * Wizard and a New Template Wizard: it has the "Activity selector" page from
+ * the New Project Wizard, which is used to dynamically select a wizard for the
+ * second page, but beyond that it runs the normal template wizard when it comes
+ * time to create the template.
+ */
+public class NewActivityWizard extends TemplateWizard {
+ private NewTemplatePage mTemplatePage;
+ private ActivityPage mActivityPage;
+ private NewProjectWizardState mValues;
+ private NewTemplateWizardState mActivityValues;
+ protected boolean mOnlyActivities;
+
+ /** Creates a new {@link NewActivityWizard} */
+ public NewActivityWizard() {
+ mOnlyActivities = true;
+ }
+
+ @Override
+ protected boolean shouldAddIconPage() {
+ return mActivityValues.getIconState() != null;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ setWindowTitle(mOnlyActivities ? "New Activity" : "New Android Object");
+
+ mValues = new NewProjectWizardState();
+ mActivityPage = new ActivityPage(mValues, mOnlyActivities, false);
+
+ mActivityValues = mValues.activityValues;
+ List<IProject> projects = AdtUtils.getSelectedProjects(selection);
+ if (projects.size() == 1) {
+ mActivityValues.project = projects.get(0);
+ }
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mActivityPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mActivityPage) {
+ if (mTemplatePage == null) {
+ Set<String> hidden = mActivityValues.hidden;
+ hidden.add(ATTR_PACKAGE_NAME);
+ hidden.add(ATTR_MIN_API);
+ hidden.add(ATTR_MIN_API_LEVEL);
+ hidden.add(ATTR_TARGET_API);
+ hidden.add(ATTR_BUILD_API);
+
+ mTemplatePage = new NewTemplatePage(mActivityValues, true);
+ addPage(mTemplatePage);
+ }
+ return mTemplatePage;
+ } else if (page == mTemplatePage && shouldAddIconPage()) {
+ WizardPage iconPage = getIconPage(mActivityValues.getIconState());
+ mActivityValues.updateIconState(mTemplatePage.getEvaluator());
+ return iconPage;
+ } else if (page == mTemplatePage
+ || shouldAddIconPage() && page == getIconPage(mActivityValues.getIconState())) {
+ TemplateMetadata template = mActivityValues.getTemplateHandler().getTemplate();
+ if (template != null) {
+ if (InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getPreviewPage(mActivityValues);
+ } else {
+ return getDependencyPage(template, true);
+ }
+ }
+ } else {
+ TemplateMetadata template = mActivityValues.getTemplateHandler().getTemplate();
+ if (template != null && page == getDependencyPage(template, false)) {
+ return getPreviewPage(mActivityValues);
+ }
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ public boolean canFinish() {
+ // Deal with lazy creation of some pages: these may not be in the page-list yet
+ // since they are constructed lazily, so consider that option here.
+ if (mTemplatePage == null || !mTemplatePage.isPageComplete()) {
+ return false;
+ }
+
+ return super.canFinish();
+ }
+
+ @Override
+ public boolean performFinish(IProgressMonitor monitor) throws InvocationTargetException {
+ boolean success = super.performFinish(monitor);
+
+ if (success) {
+ List<Runnable> finalizingTasks = getFinalizingActions();
+ for (Runnable r : finalizingTasks) {
+ r.run();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @NonNull
+ protected IProject getProject() {
+ return mActivityValues.project;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ TemplateHandler activityTemplate = mActivityValues.getTemplateHandler();
+ return activityTemplate.getFilesToOpen();
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ TemplateHandler activityTemplate = mActivityValues.getTemplateHandler();
+ return activityTemplate.getFinalizingActions();
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ return mActivityValues.computeChanges();
+ }
+
+ /** Wizard for creating other Android components */
+ public static class OtherWizard extends NewActivityWizard {
+ /** Create new {@link OtherWizard} */
+ public OtherWizard() {
+ mOnlyActivities = false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java
new file mode 100644
index 000000000..14f59c00d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java
@@ -0,0 +1,931 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.ide.eclipse.adt.AdtUtils.extractClassName;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplatePage.WIZARD_PAGE_WIDTH;
+
+import com.android.annotations.Nullable;
+import com.android.sdklib.SdkVersionInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.ProjectNamePage;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+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.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.jface.fieldassist.FieldDecoration;
+import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * First wizard page in the "New Project From Template" wizard
+ */
+public class NewProjectPage extends WizardPage
+ implements ModifyListener, SelectionListener, FocusListener {
+ private static final int FIELD_WIDTH = 300;
+ private static final String SAMPLE_PACKAGE_PREFIX = "com.example."; //$NON-NLS-1$
+ /** Suffix added by default to activity names */
+ static final String ACTIVITY_NAME_SUFFIX = "Activity"; //$NON-NLS-1$
+ /** Prefix added to default layout names */
+ static final String LAYOUT_NAME_PREFIX = "activity_"; //$NON-NLS-1$
+ private static final int INITIAL_MIN_SDK = 8;
+
+ private final NewProjectWizardState mValues;
+ private Map<String, Integer> mMinNameToApi;
+ private Parameter mThemeParameter;
+ private Combo mThemeCombo;
+
+ private Text mProjectText;
+ private Text mPackageText;
+ private Text mApplicationText;
+ private Combo mMinSdkCombo;
+ private Combo mTargetSdkCombo;
+ private Combo mBuildSdkCombo;
+ private Label mHelpIcon;
+ private Label mTipLabel;
+
+ private boolean mIgnore;
+ private ControlDecoration mApplicationDec;
+ private ControlDecoration mProjectDec;
+ private ControlDecoration mPackageDec;
+ private ControlDecoration mBuildTargetDec;
+ private ControlDecoration mMinSdkDec;
+ private ControlDecoration mTargetSdkDec;
+ private ControlDecoration mThemeDec;
+
+ NewProjectPage(NewProjectWizardState values) {
+ super("newAndroidApp"); //$NON-NLS-1$
+ mValues = values;
+ setTitle("New Android Application");
+ setDescription("Creates a new Android Application");
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ GridLayout gl_container = new GridLayout(4, false);
+ gl_container.horizontalSpacing = 10;
+ container.setLayout(gl_container);
+
+ Label applicationLabel = new Label(container, SWT.NONE);
+ applicationLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ applicationLabel.setText("Application Name:");
+
+ mApplicationText = new Text(container, SWT.BORDER);
+ GridData gdApplicationText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdApplicationText.widthHint = FIELD_WIDTH;
+ mApplicationText.setLayoutData(gdApplicationText);
+ mApplicationText.addModifyListener(this);
+ mApplicationText.addFocusListener(this);
+ mApplicationDec = createFieldDecoration(mApplicationText,
+ "The application name is shown in the Play Store, as well as in the " +
+ "Manage Application list in Settings.");
+
+ Label projectLabel = new Label(container, SWT.NONE);
+ projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ projectLabel.setText("Project Name:");
+ mProjectText = new Text(container, SWT.BORDER);
+ GridData gdProjectText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdProjectText.widthHint = FIELD_WIDTH;
+ mProjectText.setLayoutData(gdProjectText);
+ mProjectText.addModifyListener(this);
+ mProjectText.addFocusListener(this);
+ mProjectDec = createFieldDecoration(mProjectText,
+ "The project name is only used by Eclipse, but must be unique within the " +
+ "workspace. This can typically be the same as the application name.");
+
+ Label packageLabel = new Label(container, SWT.NONE);
+ packageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ packageLabel.setText("Package Name:");
+
+ mPackageText = new Text(container, SWT.BORDER);
+ GridData gdPackageText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdPackageText.widthHint = FIELD_WIDTH;
+ mPackageText.setLayoutData(gdPackageText);
+ mPackageText.addModifyListener(this);
+ mPackageText.addFocusListener(this);
+ mPackageDec = createFieldDecoration(mPackageText,
+ "The package name must be a unique identifier for your application.\n" +
+ "It is typically not shown to users, but it *must* stay the same " +
+ "for the lifetime of your application; it is how multiple versions " +
+ "of the same application are considered the \"same app\".\nThis is " +
+ "typically the reverse domain name of your organization plus one or " +
+ "more application identifiers, and it must be a valid Java package " +
+ "name.");
+ new Label(container, SWT.NONE);
+
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+
+ // Min SDK
+
+ Label minSdkLabel = new Label(container, SWT.NONE);
+ minSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ minSdkLabel.setText("Minimum Required SDK:");
+
+ mMinSdkCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gdMinSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdMinSdkCombo.widthHint = FIELD_WIDTH;
+ mMinSdkCombo.setLayoutData(gdMinSdkCombo);
+
+ // Pick most recent platform
+ IAndroidTarget[] targets = getCompilationTargets();
+ mMinNameToApi = Maps.newHashMap();
+ List<String> targetLabels = new ArrayList<String>(targets.length);
+ for (IAndroidTarget target : targets) {
+ String targetLabel;
+ if (target.isPlatform()
+ && target.getVersion().getApiLevel() <= AdtUtils.getHighestKnownApiLevel()) {
+ targetLabel = AdtUtils.getAndroidName(target.getVersion().getApiLevel());
+ } else {
+ targetLabel = AdtUtils.getTargetLabel(target);
+ }
+ targetLabels.add(targetLabel);
+ mMinNameToApi.put(targetLabel, target.getVersion().getApiLevel());
+ }
+
+ List<String> codeNames = Lists.newArrayList();
+ int buildTargetIndex = -1;
+ for (int i = 0, n = targets.length; i < n; i++) {
+ IAndroidTarget target = targets[i];
+ AndroidVersion version = target.getVersion();
+ int apiLevel = version.getApiLevel();
+ if (version.isPreview()) {
+ String codeName = version.getCodename();
+ String targetLabel = codeName + " Preview";
+ codeNames.add(targetLabel);
+ mMinNameToApi.put(targetLabel, apiLevel);
+ } else if (target.isPlatform()
+ && (mValues.target == null ||
+ apiLevel > mValues.target.getVersion().getApiLevel())) {
+ mValues.target = target;
+ buildTargetIndex = i;
+ }
+ }
+ List<String> labels = new ArrayList<String>(24);
+ for (String label : AdtUtils.getKnownVersions()) {
+ labels.add(label);
+ }
+ assert labels.size() >= 15; // *Known* versions to ADT, not installed/available versions
+ for (String codeName : codeNames) {
+ labels.add(codeName);
+ }
+ String[] versions = labels.toArray(new String[labels.size()]);
+ mMinSdkCombo.setItems(versions);
+ if (mValues.target != null && mValues.target.getVersion().isPreview()) {
+ mValues.minSdk = mValues.target.getVersion().getCodename();
+ mMinSdkCombo.setText(mValues.minSdk);
+ mValues.iconState.minSdk = mValues.target.getVersion().getApiLevel();
+ mValues.minSdkLevel = mValues.iconState.minSdk;
+ } else {
+ mMinSdkCombo.select(INITIAL_MIN_SDK - 1);
+ mValues.minSdk = Integer.toString(INITIAL_MIN_SDK);
+ mValues.minSdkLevel = INITIAL_MIN_SDK;
+ mValues.iconState.minSdk = INITIAL_MIN_SDK;
+ }
+ mMinSdkCombo.addSelectionListener(this);
+ mMinSdkCombo.addFocusListener(this);
+ mMinSdkDec = createFieldDecoration(mMinSdkCombo,
+ "Choose the lowest version of Android that your application will support. Lower " +
+ "API levels target more devices, but means fewer features are available. By " +
+ "targeting API 8 and later, you reach approximately 95% of the market.");
+ new Label(container, SWT.NONE);
+
+ // Target SDK
+ Label targetSdkLabel = new Label(container, SWT.NONE);
+ targetSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ targetSdkLabel.setText("Target SDK:");
+
+ mTargetSdkCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gdTargetSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdTargetSdkCombo.widthHint = FIELD_WIDTH;
+ mTargetSdkCombo.setLayoutData(gdTargetSdkCombo);
+
+ mTargetSdkCombo.setItems(versions);
+ mTargetSdkCombo.select(mValues.targetSdkLevel - 1);
+
+ mTargetSdkCombo.addSelectionListener(this);
+ mTargetSdkCombo.addFocusListener(this);
+ mTargetSdkDec = createFieldDecoration(mTargetSdkCombo,
+ "Choose the highest API level that the application is known to work with. " +
+ "This attribute informs the system that you have tested against the target " +
+ "version and the system should not enable any compatibility behaviors to " +
+ "maintain your app's forward-compatibility with the target version. " +
+ "The application is still able to run on older versions " +
+ "(down to minSdkVersion). Your application may look dated if you are not " +
+ "targeting the current version.");
+ new Label(container, SWT.NONE);
+
+ // Build Version
+
+ Label buildSdkLabel = new Label(container, SWT.NONE);
+ buildSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ buildSdkLabel.setText("Compile With:");
+
+ mBuildSdkCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gdBuildSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdBuildSdkCombo.widthHint = FIELD_WIDTH;
+ mBuildSdkCombo.setLayoutData(gdBuildSdkCombo);
+ mBuildSdkCombo.setData(targets);
+ mBuildSdkCombo.setItems(targetLabels.toArray(new String[targetLabels.size()]));
+ if (buildTargetIndex != -1) {
+ mBuildSdkCombo.select(buildTargetIndex);
+ }
+
+ mBuildSdkCombo.addSelectionListener(this);
+ mBuildSdkCombo.addFocusListener(this);
+ mBuildTargetDec = createFieldDecoration(mBuildSdkCombo,
+ "Choose a target API to compile your code against, from your installed SDKs. " +
+ "This is typically the most recent version, or the first version that supports " +
+ "all the APIs you want to directly access without reflection.");
+ new Label(container, SWT.NONE);
+
+ TemplateMetadata metadata = mValues.template.getTemplate();
+ if (metadata != null) {
+ mThemeParameter = metadata.getParameter("baseTheme"); //$NON-NLS-1$
+ if (mThemeParameter != null && mThemeParameter.element != null) {
+ Label themeLabel = new Label(container, SWT.NONE);
+ themeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ themeLabel.setText("Theme:");
+
+ mThemeCombo = NewTemplatePage.createOptionCombo(mThemeParameter, container,
+ mValues.parameters, this, this);
+ GridData gdThemeCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdThemeCombo.widthHint = FIELD_WIDTH;
+ mThemeCombo.setLayoutData(gdThemeCombo);
+ new Label(container, SWT.NONE);
+
+ mThemeDec = createFieldDecoration(mThemeCombo,
+ "Choose the base theme to use for the application");
+ }
+ }
+
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+
+ Label label = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 4, 1));
+
+ mHelpIcon = new Label(container, SWT.NONE);
+ mHelpIcon.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1));
+ Image icon = IconFactory.getInstance().getIcon("quickfix");
+ mHelpIcon.setImage(icon);
+ mHelpIcon.setVisible(false);
+
+ mTipLabel = new Label(container, SWT.WRAP);
+ mTipLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
+
+ // Reserve space for 4 lines
+ mTipLabel.setText("\n\n\n\n"); //$NON-NLS-1$
+
+ // Reserve enough width to accommodate the various wizard pages up front
+ // (since they are created lazily, and we don't want the wizard to dynamically
+ // resize itself for small size adjustments as each successive page is slightly
+ // larger)
+ Label dummy = new Label(container, SWT.NONE);
+ GridData data = new GridData();
+ data.horizontalSpan = 4;
+ data.widthHint = WIZARD_PAGE_WIDTH;
+ dummy.setLayoutData(data);
+ }
+
+ /**
+ * Updates the theme selection such that it's valid for the current build
+ * and min sdk targets. Also runs {@link #validatePage} in case no valid entry was found.
+ * Does nothing if called on a template that does not supply a theme.
+ */
+ void updateTheme() {
+ if (mThemeParameter != null) {
+ // Pick the highest theme version that works for the current SDK level
+ Parameter parameter = NewTemplatePage.getParameter(mThemeCombo);
+ assert parameter == mThemeParameter;
+ if (parameter != null) {
+ String[] optionIds = (String[]) mThemeCombo.getData(ATTR_ID);
+ for (int index = optionIds.length - 1; index >= 0; index--) {
+ IStatus status = NewTemplatePage.validateCombo(null, mThemeParameter,
+ index, mValues.minSdkLevel, mValues.getBuildApi());
+ if (status == null || status.isOK()) {
+ String optionId = optionIds[index];
+ parameter.value = optionId;
+ parameter.edited = optionId != null && !optionId.toString().isEmpty();
+ mValues.parameters.put(parameter.id, optionId);
+ try {
+ mIgnore = true;
+ mThemeCombo.select(index);
+ } finally {
+ mIgnore = false;
+ }
+ break;
+ }
+ }
+ }
+
+ validatePage();
+ }
+ }
+
+ private IAndroidTarget[] getCompilationTargets() {
+ Sdk current = Sdk.getCurrent();
+ if (current == null) {
+ return new IAndroidTarget[0];
+ }
+ IAndroidTarget[] targets = current.getTargets();
+ List<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
+
+ for (IAndroidTarget target : targets) {
+ if (target.isPlatform() == false &&
+ (target.getOptionalLibraries() == null ||
+ target.getOptionalLibraries().length == 0)) {
+ continue;
+ }
+ list.add(target);
+ }
+
+ return list.toArray(new IAndroidTarget[list.size()]);
+ }
+
+ private ControlDecoration createFieldDecoration(Control control, String description) {
+ ControlDecoration dec = new ControlDecoration(control, SWT.LEFT);
+ dec.setMarginWidth(2);
+ FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
+ getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION);
+ dec.setImage(errorFieldIndicator.getImage());
+ dec.setDescriptionText(description);
+ control.setToolTipText(description);
+
+ return dec;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ // DURING DEVELOPMENT ONLY
+ //if (assertionsEnabled()) {
+ // String uniqueProjectName = AdtUtils.getUniqueProjectName("Test", "");
+ // mProjectText.setText(uniqueProjectName);
+ // mPackageText.setText("test.pkg");
+ //}
+
+ validatePage();
+ }
+
+ // ---- Implements ModifyListener ----
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mProjectText) {
+ mValues.projectName = mProjectText.getText();
+ updateProjectLocation(mValues.projectName);
+ mValues.projectModified = true;
+
+ try {
+ mIgnore = true;
+ if (!mValues.applicationModified) {
+ mValues.applicationName = mValues.projectName;
+ mApplicationText.setText(mValues.projectName);
+ }
+ updateActivityNames(mValues.projectName);
+ } finally {
+ mIgnore = false;
+ }
+ suggestPackage(mValues.projectName);
+ } else if (source == mPackageText) {
+ mValues.packageName = mPackageText.getText();
+ mValues.packageModified = true;
+ } else if (source == mApplicationText) {
+ mValues.applicationName = mApplicationText.getText();
+ mValues.applicationModified = true;
+
+ try {
+ mIgnore = true;
+ if (!mValues.projectModified) {
+ mValues.projectName = appNameToProjectName(mValues.applicationName);
+ mProjectText.setText(mValues.projectName);
+ updateProjectLocation(mValues.projectName);
+ }
+ updateActivityNames(mValues.applicationName);
+ } finally {
+ mIgnore = false;
+ }
+ suggestPackage(mValues.applicationName);
+ }
+
+ validatePage();
+ }
+
+ private String appNameToProjectName(String appName) {
+ // Strip out whitespace (and capitalize subsequent words where spaces were removed
+ boolean upcaseNext = false;
+ StringBuilder sb = new StringBuilder(appName.length());
+ for (int i = 0, n = appName.length(); i < n; i++) {
+ char c = appName.charAt(i);
+ if (c == ' ') {
+ upcaseNext = true;
+ } else if (upcaseNext) {
+ sb.append(Character.toUpperCase(c));
+ upcaseNext = false;
+ } else {
+ sb.append(c);
+ }
+ }
+
+ appName = sb.toString().trim();
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IStatus nameStatus = workspace.validateName(appName, IResource.PROJECT);
+ if (nameStatus.isOK()) {
+ return appName;
+ }
+
+ sb = new StringBuilder(appName.length());
+ for (int i = 0, n = appName.length(); i < n; i++) {
+ char c = appName.charAt(i);
+ if (Character.isLetterOrDigit(c) || c == '.' || c == '-') {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString().trim();
+ }
+
+ /** If the project should be created in the workspace, then update the project location
+ * based on the project name. */
+ private void updateProjectLocation(String projectName) {
+ if (projectName == null) {
+ projectName = "";
+ }
+
+ if (mValues.useDefaultLocation) {
+ IPath workspace = Platform.getLocation();
+ String projectLocation = workspace.append(projectName).toOSString();
+ mValues.projectLocation = projectLocation;
+ }
+ }
+
+ private void updateActivityNames(String name) {
+ try {
+ mIgnore = true;
+ if (!mValues.activityNameModified) {
+ mValues.activityName = extractClassName(name) + ACTIVITY_NAME_SUFFIX;
+ }
+ if (!mValues.activityTitleModified) {
+ mValues.activityTitle = name;
+ }
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mMinSdkCombo) {
+ mValues.minSdk = getSelectedMinSdk();
+ Integer minSdk = mMinNameToApi.get(mValues.minSdk);
+ if (minSdk == null) {
+ try {
+ minSdk = Integer.parseInt(mValues.minSdk);
+ } catch (NumberFormatException nufe) {
+ // If not a number, then the string is a codename, so treat it
+ // as a preview version.
+ minSdk = SdkVersionInfo.HIGHEST_KNOWN_API + 1;
+ }
+ }
+ mValues.iconState.minSdk = minSdk.intValue();
+ mValues.minSdkLevel = minSdk.intValue();
+
+ // If higher than build target, adjust build target
+ if (mValues.minSdkLevel > mValues.getBuildApi()) {
+ // Try to find a build target with an adequate build API
+ IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
+ IAndroidTarget best = null;
+ int bestApi = Integer.MAX_VALUE;
+ int bestTargetIndex = -1;
+ for (int i = 0; i < targets.length; i++) {
+ IAndroidTarget target = targets[i];
+ if (!target.isPlatform()) {
+ continue;
+ }
+ int api = target.getVersion().getApiLevel();
+ if (api >= mValues.minSdkLevel && api < bestApi) {
+ best = target;
+ bestApi = api;
+ bestTargetIndex = i;
+ }
+ }
+
+ if (best != null) {
+ assert bestTargetIndex != -1;
+ mValues.target = best;
+ try {
+ mIgnore = true;
+ mBuildSdkCombo.select(bestTargetIndex);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ // If higher than targetSdkVersion, adjust targetSdkVersion
+ if (mValues.minSdkLevel > mValues.targetSdkLevel) {
+ mValues.targetSdkLevel = mValues.minSdkLevel;
+ try {
+ mIgnore = true;
+ setSelectedTargetSdk(mValues.targetSdkLevel);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ } else if (source == mBuildSdkCombo) {
+ mValues.target = getSelectedBuildTarget();
+
+ // If lower than min sdk target, adjust min sdk target
+ if (mValues.target.getVersion().isPreview()) {
+ mValues.minSdk = mValues.target.getVersion().getCodename();
+ try {
+ mIgnore = true;
+ mMinSdkCombo.setText(mValues.minSdk);
+ } finally {
+ mIgnore = false;
+ }
+ } else {
+ String minSdk = mValues.minSdk;
+ int buildApiLevel = mValues.target.getVersion().getApiLevel();
+ if (minSdk != null && !minSdk.isEmpty()
+ && Character.isDigit(minSdk.charAt(0))
+ && buildApiLevel < Integer.parseInt(minSdk)) {
+ mValues.minSdk = Integer.toString(buildApiLevel);
+ try {
+ mIgnore = true;
+ setSelectedMinSdk(buildApiLevel);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+ } else if (source == mTargetSdkCombo) {
+ mValues.targetSdkLevel = getSelectedTargetSdk();
+ }
+
+ validatePage();
+ }
+
+ private String getSelectedMinSdk() {
+ // If you're using a preview build, such as android-JellyBean, you have
+ // to use the codename, e.g. JellyBean, as the minimum SDK as well.
+ IAndroidTarget buildTarget = getSelectedBuildTarget();
+ if (buildTarget != null && buildTarget.getVersion().isPreview()) {
+ return buildTarget.getVersion().getCodename();
+ }
+
+ // +1: First API level (at index 0) is 1
+ return Integer.toString(mMinSdkCombo.getSelectionIndex() + 1);
+ }
+
+ private int getSelectedTargetSdk() {
+ // +1: First API level (at index 0) is 1
+ return mTargetSdkCombo.getSelectionIndex() + 1;
+ }
+
+ private void setSelectedMinSdk(int api) {
+ mMinSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
+ }
+
+ private void setSelectedTargetSdk(int api) {
+ mTargetSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
+ }
+
+ @Nullable
+ private IAndroidTarget getSelectedBuildTarget() {
+ IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
+ int index = mBuildSdkCombo.getSelectionIndex();
+ if (index >= 0 && index < targets.length) {
+ return targets[index];
+ } else {
+ return null;
+ }
+ }
+
+ private void suggestPackage(String original) {
+ if (!mValues.packageModified) {
+ // Create default package name
+ StringBuilder sb = new StringBuilder();
+ sb.append(SAMPLE_PACKAGE_PREFIX);
+ appendPackage(sb, original);
+
+ String pkg = sb.toString();
+ if (pkg.endsWith(".")) { //$NON-NLS-1$
+ pkg = pkg.substring(0, pkg.length() - 1);
+ }
+ mValues.packageName = pkg;
+ try {
+ mIgnore = true;
+ mPackageText.setText(mValues.packageName);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ private static void appendPackage(StringBuilder sb, String string) {
+ for (int i = 0, n = string.length(); i < n; i++) {
+ char c = string.charAt(i);
+ if (i == 0 && Character.isJavaIdentifierStart(c)
+ || i != 0 && Character.isJavaIdentifierPart(c)) {
+ sb.append(Character.toLowerCase(c));
+ } else if ((c == '.')
+ && (sb.length() > 0 && sb.charAt(sb.length() - 1) != '.')) {
+ sb.append('.');
+ } else if (c == '-') {
+ sb.append('_');
+ }
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- Implements FocusListener ----
+
+ @Override
+ public void focusGained(FocusEvent e) {
+ Object source = e.getSource();
+ String tip = "";
+ if (source == mApplicationText) {
+ tip = mApplicationDec.getDescriptionText();
+ } else if (source == mProjectText) {
+ tip = mProjectDec.getDescriptionText();
+ } else if (source == mBuildSdkCombo) {
+ tip = mBuildTargetDec.getDescriptionText();
+ } else if (source == mMinSdkCombo) {
+ tip = mMinSdkDec.getDescriptionText();
+ } else if (source == mPackageText) {
+ tip = mPackageDec.getDescriptionText();
+ if (mPackageText.getText().startsWith(SAMPLE_PACKAGE_PREFIX)) {
+ int length = SAMPLE_PACKAGE_PREFIX.length();
+ if (mPackageText.getText().length() > length
+ && SAMPLE_PACKAGE_PREFIX.endsWith(".")) { //$NON-NLS-1$
+ length--;
+ }
+ mPackageText.setSelection(0, length);
+ }
+ } else if (source == mTargetSdkCombo) {
+ tip = mTargetSdkDec.getDescriptionText();
+ } else if (source == mThemeCombo) {
+ tip = mThemeDec.getDescriptionText();
+ }
+ mTipLabel.setText(tip);
+ mHelpIcon.setVisible(tip.length() > 0);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mTipLabel.setText("");
+ mHelpIcon.setVisible(false);
+ }
+
+ // Validation
+
+ private void validatePage() {
+ IStatus status = mValues.template.validateTemplate(mValues.minSdkLevel,
+ mValues.getBuildApi());
+ if (status != null && !status.isOK()) {
+ updateDecorator(mApplicationDec, null, true);
+ updateDecorator(mPackageDec, null, true);
+ updateDecorator(mProjectDec, null, true);
+ updateDecorator(mThemeDec, null, true);
+ /* These never get marked with errors:
+ updateDecorator(mBuildTargetDec, null, true);
+ updateDecorator(mMinSdkDec, null, true);
+ updateDecorator(mTargetSdkDec, null, true);
+ */
+ } else {
+ IStatus appStatus = validateAppName();
+ if (appStatus != null && (status == null
+ || appStatus.getSeverity() > status.getSeverity())) {
+ status = appStatus;
+ }
+
+ IStatus projectStatus = validateProjectName();
+ if (projectStatus != null && (status == null
+ || projectStatus.getSeverity() > status.getSeverity())) {
+ status = projectStatus;
+ }
+
+ IStatus packageStatus = validatePackageName();
+ if (packageStatus != null && (status == null
+ || packageStatus.getSeverity() > status.getSeverity())) {
+ status = packageStatus;
+ }
+
+ IStatus locationStatus = ProjectContentsPage.validateLocationInWorkspace(mValues);
+ if (locationStatus != null && (status == null
+ || locationStatus.getSeverity() > status.getSeverity())) {
+ status = locationStatus;
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.target == null) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "Select an Android build target version");
+ }
+ }
+
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.minSdk == null || mValues.minSdk.isEmpty()) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "Select a minimum SDK version");
+ } else {
+ AndroidVersion version = mValues.target.getVersion();
+ if (version.isPreview()) {
+ if (version.getCodename().equals(mValues.minSdk) == false) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Preview platforms require the min SDK version to match their codenames.");
+ }
+ } else if (mValues.target.getVersion().compareTo(
+ mValues.minSdkLevel,
+ version.isPreview() ? mValues.minSdk : null) < 0) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "The minimum SDK version is higher than the build target version");
+ }
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.targetSdkLevel < mValues.minSdkLevel) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "The target SDK version should be at least as high as the minimum SDK version");
+ }
+ }
+ }
+ }
+
+ IStatus themeStatus = validateTheme();
+ if (themeStatus != null && (status == null
+ || themeStatus.getSeverity() > status.getSeverity())) {
+ status = themeStatus;
+ }
+ }
+
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ private IStatus validateAppName() {
+ String appName = mValues.applicationName;
+ IStatus status = null;
+ if (appName == null || appName.isEmpty()) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Enter an application name (shown in launcher)");
+ } else if (Character.isLowerCase(mValues.applicationName.charAt(0))) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "The application name for most apps begins with an uppercase letter");
+ }
+
+ updateDecorator(mApplicationDec, status, true);
+
+ return status;
+ }
+
+ private IStatus validateProjectName() {
+ IStatus status = ProjectNamePage.validateProjectName(mValues.projectName);
+ updateDecorator(mProjectDec, status, true);
+
+ return status;
+ }
+
+ private IStatus validatePackageName() {
+ IStatus status;
+ if (mValues.packageName == null || mValues.packageName.startsWith(SAMPLE_PACKAGE_PREFIX)) {
+ if (mValues.packageName != null
+ && !mValues.packageName.equals(SAMPLE_PACKAGE_PREFIX)) {
+ status = ApplicationInfoPage.validatePackage(mValues.packageName);
+ if (status == null || status.isOK()) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ String.format("The prefix '%1$s' is meant as a placeholder and should " +
+ "not be used", SAMPLE_PACKAGE_PREFIX));
+ }
+ } else {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Package name must be specified.");
+ }
+ } else {
+ status = ApplicationInfoPage.validatePackage(mValues.packageName);
+ }
+
+ updateDecorator(mPackageDec, status, true);
+
+ return status;
+ }
+
+ private IStatus validateTheme() {
+ IStatus status = null;
+
+ if (mThemeParameter != null) {
+ status = NewTemplatePage.validateCombo(null, mThemeParameter,
+ mThemeCombo.getSelectionIndex(), mValues.minSdkLevel,
+ mValues.getBuildApi());
+
+ updateDecorator(mThemeDec, status, true);
+ }
+
+ return status;
+ }
+
+ private void updateDecorator(ControlDecoration decorator, IStatus status, boolean hasInfo) {
+ if (hasInfo) {
+ int severity = status != null ? status.getSeverity() : IStatus.OK;
+ setDecoratorType(decorator, severity);
+ } else {
+ if (status == null || status.isOK()) {
+ decorator.hide();
+ } else {
+ decorator.show();
+ }
+ }
+ }
+
+ private void setDecoratorType(ControlDecoration decorator, int severity) {
+ String id;
+ if (severity == IStatus.ERROR) {
+ id = FieldDecorationRegistry.DEC_ERROR;
+ } else if (severity == IStatus.WARNING) {
+ id = FieldDecorationRegistry.DEC_WARNING;
+ } else {
+ id = FieldDecorationRegistry.DEC_INFORMATION;
+ }
+ FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
+ getFieldDecoration(id);
+ decorator.setImage(errorFieldIndicator.getImage());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java
new file mode 100644
index 000000000..d350a00dd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static org.eclipse.core.resources.IResource.DEPTH_INFINITE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.assetstudiolib.GraphicGenerator;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.ide.eclipse.adt.internal.assetstudio.AssetType;
+import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator.ProjectPopulator;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Wizard for creating new projects
+ */
+public class NewProjectWizard extends TemplateWizard {
+ private static final String PARENT_ACTIVITY_CLASS = "parentActivityClass"; //$NON-NLS-1$
+ private static final String ACTIVITY_TITLE = "activityTitle"; //$NON-NLS-1$
+ static final String IS_LAUNCHER = "isLauncher"; //$NON-NLS-1$
+ static final String IS_NEW_PROJECT = "isNewProject"; //$NON-NLS-1$
+ static final String IS_LIBRARY_PROJECT = "isLibraryProject"; //$NON-NLS-1$
+ static final String ATTR_COPY_ICONS = "copyIcons"; //$NON-NLS-1$
+ static final String ATTR_TARGET_API = "targetApi"; //$NON-NLS-1$
+ static final String ATTR_MIN_API = "minApi"; //$NON-NLS-1$
+ static final String ATTR_MIN_BUILD_API = "minBuildApi"; //$NON-NLS-1$
+ static final String ATTR_BUILD_API = "buildApi"; //$NON-NLS-1$
+ static final String ATTR_REVISION = "revision"; //$NON-NLS-1$
+ static final String ATTR_MIN_API_LEVEL = "minApiLevel"; //$NON-NLS-1$
+ static final String ATTR_PACKAGE_NAME = "packageName"; //$NON-NLS-1$
+ static final String ATTR_APP_TITLE = "appTitle"; //$NON-NLS-1$
+ static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$
+ static final String CATEGORY_ACTIVITIES = "activities"; //$NON-NLS-1$
+ static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$
+ static final String ATTR_APP_COMPAT = "appCompat"; //$NON-NLS-1$
+ /**
+ * Reserved file name for the launcher icon, resolves to the xhdpi version
+ *
+ * @see CreateAssetSetWizardState#getImage
+ */
+ public static final String DEFAULT_LAUNCHER_ICON = "launcher_icon"; //$NON-NLS-1$
+
+ private NewProjectPage mMainPage;
+ private ProjectContentsPage mContentsPage;
+ private ActivityPage mActivityPage;
+ private NewTemplatePage mTemplatePage;
+ private NewProjectWizardState mValues;
+ /** The project being created */
+ private IProject mProject;
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ setWindowTitle("New Android Application");
+
+ mValues = new NewProjectWizardState();
+ mMainPage = new NewProjectPage(mValues);
+ mContentsPage = new ProjectContentsPage(mValues);
+ mContentsPage.init(selection, AdtUtils.getActivePart());
+ mActivityPage = new ActivityPage(mValues, true, true);
+ mActivityPage.setLauncherActivitiesOnly(true);
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mMainPage);
+ addPage(mContentsPage);
+ addPage(mActivityPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mMainPage) {
+ return mContentsPage;
+ }
+
+ if (page == mContentsPage) {
+ if (mValues.createIcon) {
+ // Bundle asset studio wizard to create the launcher icon
+ CreateAssetSetWizardState iconState = mValues.iconState;
+ iconState.type = AssetType.LAUNCHER;
+ iconState.outputName = "ic_launcher"; //$NON-NLS-1$
+ iconState.background = new RGB(0xff, 0xff, 0xff);
+ iconState.foreground = new RGB(0x33, 0xb6, 0xea);
+ iconState.trim = true;
+
+ // ADT 20: White icon with blue shape
+ //iconState.shape = GraphicGenerator.Shape.CIRCLE;
+ //iconState.sourceType = CreateAssetSetWizardState.SourceType.CLIPART;
+ //iconState.clipartName = "user.png"; //$NON-NLS-1$
+ //iconState.padding = 10;
+
+ // ADT 21: Use the platform packaging icon, but allow user to customize it
+ iconState.sourceType = CreateAssetSetWizardState.SourceType.IMAGE;
+ iconState.imagePath = new File(DEFAULT_LAUNCHER_ICON);
+ iconState.shape = GraphicGenerator.Shape.NONE;
+ iconState.padding = 0;
+
+ WizardPage p = getIconPage(mValues.iconState);
+ p.setTitle("Configure Launcher Icon");
+ return p;
+ } else {
+ if (mValues.createActivity) {
+ return mActivityPage;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ if (page == mIconPage) {
+ return mActivityPage;
+ }
+
+ if (page == mActivityPage && mValues.createActivity) {
+ if (mTemplatePage == null) {
+ NewTemplateWizardState activityValues = mValues.activityValues;
+
+ // Initialize the *default* activity name based on what we've derived
+ // from the project name
+ activityValues.defaults.put("activityName", mValues.activityName);
+
+ // Hide those parameters that the template requires but that we don't want to
+ // ask the users about, since we will supply these values from the rest
+ // of the new project wizard.
+ Set<String> hidden = activityValues.hidden;
+ hidden.add(ATTR_PACKAGE_NAME);
+ hidden.add(ATTR_APP_TITLE);
+ hidden.add(ATTR_MIN_API);
+ hidden.add(ATTR_MIN_API_LEVEL);
+ hidden.add(ATTR_TARGET_API);
+ hidden.add(ATTR_BUILD_API);
+ hidden.add(IS_LAUNCHER);
+ // Don't ask about hierarchical parent activities in new projects where there
+ // can't possibly be any
+ hidden.add(PARENT_ACTIVITY_CLASS);
+ hidden.add(ACTIVITY_TITLE); // Not used for the first activity in the project
+
+ mTemplatePage = new NewTemplatePage(activityValues, false);
+ addPage(mTemplatePage);
+ }
+ mTemplatePage.setCustomMinSdk(mValues.minSdkLevel, mValues.getBuildApi());
+ return mTemplatePage;
+ }
+
+ if (page == mTemplatePage) {
+ TemplateMetadata template = mValues.activityValues.getTemplateHandler().getTemplate();
+ if (template != null
+ && !InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getDependencyPage(template, true);
+ }
+ }
+
+ if (page == mTemplatePage || !mValues.createActivity && page == mActivityPage
+ || page == getDependencyPage(null, false)) {
+ return null;
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ public boolean canFinish() {
+ // Deal with lazy creation of some pages: these may not be in the page-list yet
+ // since they are constructed lazily, so consider that option here.
+ if (mValues.createIcon && (mIconPage == null || !mIconPage.isPageComplete())) {
+ return false;
+ }
+ if (mValues.createActivity && (mTemplatePage == null || !mTemplatePage.isPageComplete())) {
+ return false;
+ }
+
+ // Override super behavior (which just calls isPageComplete() on each of the pages)
+ // to special case the template and icon pages since we want to skip them if
+ // the appropriate flags are not set.
+ for (IWizardPage page : getPages()) {
+ if (page == mTemplatePage && !mValues.createActivity) {
+ continue;
+ }
+ if (page == mIconPage && !mValues.createIcon) {
+ continue;
+ }
+ if (!page.isPageComplete()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ @NonNull
+ protected IProject getProject() {
+ return mProject;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ return mValues.template.getFilesToOpen();
+ }
+
+ @VisibleForTesting
+ NewProjectWizardState getValues() {
+ return mValues;
+ }
+
+ @VisibleForTesting
+ void setValues(NewProjectWizardState values) {
+ mValues = values;
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ final TemplateHandler template = mValues.template;
+ // We'll be merging in an activity template, but don't create *~ backup files
+ // of the merged files (such as the manifest file) in that case.
+ // (NOTE: After the change from direct file manipulation to creating a list of Change
+ // objects, this no longer applies - but the code is kept around a little while longer
+ // in case we want to generate change objects that makes backups of merged files)
+ template.setBackupMergedFiles(false);
+
+ // Generate basic output skeleton
+ Map<String, Object> paramMap = new HashMap<String, Object>();
+ addProjectInfo(paramMap);
+ TemplateHandler.addDirectoryParameters(paramMap, getProject());
+ // We don't know at this point whether the activity is going to need
+ // AppCompat so we just assume that it will.
+ if (mValues.createActivity && mValues.minSdkLevel < 14) {
+ paramMap.put(ATTR_APP_COMPAT, true);
+ getFinalizingActions().add(new Runnable() {
+ @Override
+ public void run() {
+ AddSupportJarAction.installAppCompatLibrary(mProject, true);
+ }
+ });
+ }
+
+ return template.render(mProject, paramMap);
+ }
+
+ @Override
+ protected boolean performFinish(final IProgressMonitor monitor)
+ throws InvocationTargetException {
+ try {
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ String name = mValues.projectName;
+ mProject = root.getProject(name);
+
+ final TemplateHandler template = mValues.template;
+ // We'll be merging in an activity template, but don't create *~ backup files
+ // of the merged files (such as the manifest file) in that case.
+ template.setBackupMergedFiles(false);
+
+ ProjectPopulator projectPopulator = new ProjectPopulator() {
+ @Override
+ public void populate(IProject project) throws InvocationTargetException {
+ // Copy in the proguard file; templates don't provide this one.
+ // add the default proguard config
+ File libFolder = new File(AdtPlugin.getOsSdkToolsFolder(),
+ SdkConstants.FD_LIB);
+ try {
+ assert project == mProject;
+ NewProjectCreator.addLocalFile(project,
+ new File(libFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE),
+ // Write ProGuard config files with the extension .pro which
+ // is what is used in the ProGuard documentation and samples
+ SdkConstants.FN_PROJECT_PROGUARD_FILE,
+ new NullProgressMonitor());
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+
+ try {
+ mProject.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ // Render the project template
+ List<Change> changes = computeChanges();
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating project...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ if (mValues.createIcon) { // TODO: Set progress
+ generateIcons(mProject);
+ }
+
+ // Render the embedded activity template template
+ if (mValues.createActivity) {
+ final TemplateHandler activityTemplate =
+ mValues.activityValues.getTemplateHandler();
+ // We'll be merging in an activity template, but don't create
+ // *~ backup files of the merged files (such as the manifest file)
+ // in that case.
+ activityTemplate.setBackupMergedFiles(false);
+ generateActivity(template, project, monitor);
+ }
+ }
+ };
+
+ NewProjectCreator.create(monitor, mProject, mValues.target, projectPopulator,
+ mValues.isLibrary, mValues.projectLocation, mValues.workingSets);
+
+ // For new projects, ensure that we're actually using the preferred compliance,
+ // not just the default one
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject != null) {
+ ProjectHelper.enforcePreferredCompilerCompliance(javaProject);
+ }
+
+ try {
+ mProject.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ List<Runnable> finalizingTasks = getFinalizingActions();
+ for (Runnable r : finalizingTasks) {
+ r.run();
+ }
+
+ return true;
+ } catch (Exception ioe) {
+ AdtPlugin.log(ioe, null);
+ return false;
+ }
+ }
+
+ /**
+ * Generate custom icons into the project based on the asset studio wizard state
+ */
+ private void generateIcons(final IProject newProject) {
+ // Generate the custom icons
+ assert mValues.createIcon;
+ ConfigureAssetSetPage.generateIcons(newProject, mValues.iconState, false, mIconPage);
+ }
+
+ /**
+ * Generate the activity: Pre-populate information about the project the
+ * activity needs but that we don't need to ask about when creating a new
+ * project
+ */
+ private void generateActivity(TemplateHandler projectTemplate, IProject project,
+ IProgressMonitor monitor) throws InvocationTargetException {
+ assert mValues.createActivity;
+ NewTemplateWizardState activityValues = mValues.activityValues;
+ Map<String, Object> parameters = activityValues.parameters;
+
+ addProjectInfo(parameters);
+
+ parameters.put(IS_NEW_PROJECT, true);
+ parameters.put(IS_LIBRARY_PROJECT, mValues.isLibrary);
+ // Ensure that activities created as part of a new project are marked as
+ // launcher activities
+ parameters.put(IS_LAUNCHER, true);
+ TemplateHandler.addDirectoryParameters(parameters, project);
+
+ TemplateHandler activityTemplate = activityValues.getTemplateHandler();
+ activityTemplate.setBackupMergedFiles(false);
+ List<Change> changes = activityTemplate.render(project, parameters);
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating template...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ List<String> filesToOpen = activityTemplate.getFilesToOpen();
+ projectTemplate.getFilesToOpen().addAll(filesToOpen);
+
+ List<Runnable> finalizingActions = activityTemplate.getFinalizingActions();
+ projectTemplate.getFinalizingActions().addAll(finalizingActions);
+ }
+
+ private void addProjectInfo(Map<String, Object> parameters) {
+ parameters.put(ATTR_PACKAGE_NAME, mValues.packageName);
+ parameters.put(ATTR_APP_TITLE, mValues.applicationName);
+ parameters.put(ATTR_MIN_API, mValues.minSdk);
+ parameters.put(ATTR_MIN_API_LEVEL, mValues.minSdkLevel);
+ parameters.put(ATTR_TARGET_API, mValues.targetSdkLevel);
+ parameters.put(ATTR_BUILD_API, mValues.target.getVersion().getApiLevel());
+ parameters.put(ATTR_COPY_ICONS, !mValues.createIcon);
+ parameters.putAll(mValues.parameters);
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ return mValues.template.getFinalizingActions();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java
new file mode 100644
index 000000000..9cd3a6dcf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_PROJECTS;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.ui.IWorkingSet;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Value object which holds the current state of the wizard pages for the
+ * {@link NewProjectWizard}
+ */
+public class NewProjectWizardState {
+ /** Creates a new {@link NewProjectWizardState} */
+ public NewProjectWizardState() {
+ template = TemplateHandler.createFromName(CATEGORY_PROJECTS,
+ "NewAndroidApplication"); //$NON-NLS-1$
+ }
+
+ /** The template handler instantiating the project */
+ public final TemplateHandler template;
+
+ /** The name of the project */
+ public String projectName;
+
+ /** The derived name of the activity, if any */
+ public String activityName;
+
+ /** The derived title of the activity, if any */
+ public String activityTitle;
+
+ /** The application name */
+ public String applicationName;
+
+ /** The package name */
+ public String packageName;
+
+ /** Whether the project name has been edited by the user */
+ public boolean projectModified;
+
+ /** Whether the package name has been edited by the user */
+ public boolean packageModified;
+
+ /** Whether the activity name has been edited by the user */
+ public boolean activityNameModified;
+
+ /** Whether the activity title has been edited by the user */
+ public boolean activityTitleModified;
+
+ /** Whether the application name has been edited by the user */
+ public boolean applicationModified;
+
+ /** The compilation target to use for this project */
+ public IAndroidTarget target;
+
+ /** The minimum SDK API level, as a string (if the API is a preview release with a codename) */
+ public String minSdk;
+
+ /** The minimum SDK API level to use */
+ public int minSdkLevel;
+
+ /** The target SDK level */
+ public int targetSdkLevel = AdtUtils.getHighestKnownApiLevel();
+
+ /** Whether this project should be marked as a library project */
+ public boolean isLibrary;
+
+ /** Whether to create an activity (if so, the activity state is stored in
+ * {@link #activityValues}) */
+ public boolean createActivity = true;
+
+ /** Whether a custom icon should be created instead of just reusing the default (if so,
+ * the icon wizard state is stored in {@link #iconState}) */
+ public boolean createIcon = true;
+
+ // Delegated wizards
+
+ /** State for the asset studio wizard, used to create custom icons */
+ public CreateAssetSetWizardState iconState = new CreateAssetSetWizardState();
+
+ /** State for the template wizard, used to embed an activity template */
+ public NewTemplateWizardState activityValues = new NewTemplateWizardState();
+
+ /** Whether a custom location should be used */
+ public boolean useDefaultLocation = true;
+
+ /** Folder where the project should be created. */
+ public String projectLocation;
+
+ /** Configured parameters, by id */
+ public final Map<String, Object> parameters = new HashMap<String, Object>();
+
+ /** The set of chosen working sets to use when creating the project */
+ public IWorkingSet[] workingSets = new IWorkingSet[0];
+
+ /**
+ * Returns the build target API level
+ *
+ * @return the build target API level
+ */
+ public int getBuildApi() {
+ return target != null ? target.getVersion().getApiLevel() : minSdkLevel;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java
new file mode 100644
index 000000000..57cf5c824
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.SdkConstants.CLASS_ACTIVITY;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_ID;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo;
+import com.android.ide.eclipse.adt.internal.wizards.templates.Parameter.Constraint;
+import com.android.ide.eclipse.adt.internal.wizards.templates.Parameter.Type;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.ui.IJavaElementSearchConstants;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
+import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.jface.fieldassist.FieldDecoration;
+import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.ByteArrayInputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * First wizard page in the "New Project From Template" wizard (which is parameterized
+ * via template.xml files)
+ */
+public class NewTemplatePage extends WizardPage
+ implements ModifyListener, SelectionListener, FocusListener {
+ /** The default width to use for the wizard page */
+ static final int WIZARD_PAGE_WIDTH = 600;
+
+ private final NewTemplateWizardState mValues;
+ private final boolean mChooseProject;
+ private int mCustomMinSdk = -1;
+ private int mCustomBuildApi = -1;
+ private boolean mIgnore;
+ private boolean mShown;
+ private Control mFirst;
+ // TODO: Move decorators to the Parameter objects?
+ private Map<String, ControlDecoration> mDecorations = new HashMap<String, ControlDecoration>();
+ private Label mHelpIcon;
+ private Label mTipLabel;
+ private ImageControl mPreview;
+ private Image mPreviewImage;
+ private boolean mDisposePreviewImage;
+ private ProjectCombo mProjectButton;
+ private StringEvaluator mEvaluator;
+
+ private TemplateMetadata mShowingTemplate;
+
+ /**
+ * Creates a new {@link NewTemplatePage}
+ *
+ * @param values the wizard state
+ * @param chooseProject whether the wizard should present a project chooser,
+ * and update {@code values}' project field
+ */
+ NewTemplatePage(NewTemplateWizardState values, boolean chooseProject) {
+ super("newTemplatePage"); //$NON-NLS-1$
+ mValues = values;
+ mChooseProject = chooseProject;
+ }
+
+ /**
+ * @param minSdk a minimum SDK to use, provided chooseProject is false. If
+ * it is true, then the minimum SDK used for validation will be
+ * the one of the project
+ * @param buildApi the build API to use
+ */
+ void setCustomMinSdk(int minSdk, int buildApi) {
+ assert !mChooseProject;
+ //assert buildApi >= minSdk;
+ mCustomMinSdk = minSdk;
+ mCustomBuildApi = buildApi;
+ }
+
+ @Override
+ public void createControl(Composite parent2) {
+ Composite parent = new Composite(parent2, SWT.NULL);
+ setControl(parent);
+ GridLayout parentLayout = new GridLayout(3, false);
+ parentLayout.verticalSpacing = 0;
+ parentLayout.marginWidth = 0;
+ parentLayout.marginHeight = 0;
+ parentLayout.horizontalSpacing = 0;
+ parent.setLayout(parentLayout);
+
+ // Reserve enough width (since the panel is created lazily later)
+ Label label = new Label(parent, SWT.NONE);
+ GridData data = new GridData();
+ data.widthHint = WIZARD_PAGE_WIDTH;
+ label.setLayoutData(data);
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ private void onEnter() {
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+ if (template == mShowingTemplate) {
+ return;
+ }
+ mShowingTemplate = template;
+
+ Composite parent = (Composite) getControl();
+
+ Control[] children = parent.getChildren();
+ if (children.length > 0) {
+ for (Control c : parent.getChildren()) {
+ c.dispose();
+ }
+ for (ControlDecoration decoration : mDecorations.values()) {
+ decoration.dispose();
+ }
+ mDecorations.clear();
+ }
+
+ Composite container = new Composite(parent, SWT.NULL);
+ container.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+ GridLayout gl_container = new GridLayout(3, false);
+ gl_container.horizontalSpacing = 10;
+ container.setLayout(gl_container);
+
+ if (mChooseProject) {
+ // Project: [button]
+ String tooltip = "The Android Project where the new resource will be created.";
+ Label projectLabel = new Label(container, SWT.NONE);
+ projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ projectLabel.setText("Project:");
+ projectLabel.setToolTipText(tooltip);
+
+ ProjectChooserHelper helper =
+ new ProjectChooserHelper(getShell(), null /* filter */);
+ mProjectButton = new ProjectCombo(helper, container, mValues.project);
+ mProjectButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+ mProjectButton.setToolTipText(tooltip);
+ mProjectButton.addSelectionListener(this);
+
+ //Label projectSeparator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
+ //projectSeparator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1));
+ }
+
+ // Add parameters
+ mFirst = null;
+ String thumb = null;
+ if (template != null) {
+ thumb = template.getThumbnailPath();
+ String title = template.getTitle();
+ if (title != null && !title.isEmpty()) {
+ setTitle(title);
+ }
+ String description = template.getDescription();
+ if (description != null && !description.isEmpty()) {
+ setDescription(description);
+ }
+
+ Map<String, String> defaults = mValues.defaults;
+ Set<String> seen = null;
+ if (LintUtils.assertionsEnabled()) {
+ seen = new HashSet<String>();
+ }
+
+ List<Parameter> parameters = template.getParameters();
+ for (Parameter parameter : parameters) {
+ Parameter.Type type = parameter.type;
+
+ if (type == Parameter.Type.SEPARATOR) {
+ Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL);
+ separator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1));
+ continue;
+ }
+
+ String id = parameter.id;
+ assert id != null && !id.isEmpty() : ATTR_ID;
+ Object value = defaults.get(id);
+ if (value == null) {
+ value = parameter.value;
+ }
+
+ String name = parameter.name;
+ String help = parameter.help;
+
+ // Required
+ assert name != null && !name.isEmpty() : ATTR_NAME;
+ // Ensure id's are unique:
+ assert seen != null && seen.add(id) : id;
+
+ // Skip attributes that were already provided by the surrounding
+ // context. For example, when adding into an existing project,
+ // provide the minimum SDK automatically from the project.
+ if (mValues.hidden != null && mValues.hidden.contains(id)) {
+ continue;
+ }
+
+ switch (type) {
+ case STRING: {
+ // TODO: Look at the constraints to add validators here
+ // TODO: If I type.equals("layout") add resource validator for layout
+ // names
+ // TODO: If I type.equals("class") make class validator
+
+ // TODO: Handle package and id better later
+ Label label = new Label(container, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false,
+ 1, 1));
+ label.setText(name);
+
+ Text text = new Text(container, SWT.BORDER);
+ text.setData(parameter);
+ parameter.control = text;
+
+ if (parameter.constraints.contains(Constraint.EXISTS)) {
+ text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 1, 1));
+
+ Button button = new Button(container, SWT.FLAT);
+ button.setData(parameter);
+ button.setText("...");
+ button.addSelectionListener(this);
+ } else {
+ text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 2, 1));
+ }
+
+ boolean hasValue = false;
+ if (value instanceof String) {
+ String stringValue = (String) value;
+ hasValue = !stringValue.isEmpty();
+ text.setText(stringValue);
+ mValues.parameters.put(id, value);
+ }
+
+ if (!hasValue) {
+ if (parameter.constraints.contains(Constraint.EMPTY)) {
+ text.setMessage("Optional");
+ } else if (parameter.constraints.contains(Constraint.NONEMPTY)) {
+ text.setMessage("Required");
+ }
+ }
+
+ text.addModifyListener(this);
+ text.addFocusListener(this);
+
+ if (mFirst == null) {
+ mFirst = text;
+ }
+
+ if (help != null && !help.isEmpty()) {
+ text.setToolTipText(help);
+ ControlDecoration decoration = createFieldDecoration(id, text, help);
+ }
+ break;
+ }
+ case BOOLEAN: {
+ Label label = new Label(container, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false,
+ 1, 1));
+
+ Button checkBox = new Button(container, SWT.CHECK);
+ checkBox.setText(name);
+ checkBox.setData(parameter);
+ parameter.control = checkBox;
+ checkBox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 2, 1));
+
+ if (value instanceof Boolean) {
+ Boolean selected = (Boolean) value;
+ checkBox.setSelection(selected);
+ mValues.parameters.put(id, value);
+ }
+
+ checkBox.addSelectionListener(this);
+ checkBox.addFocusListener(this);
+
+ if (mFirst == null) {
+ mFirst = checkBox;
+ }
+
+ if (help != null && !help.isEmpty()) {
+ checkBox.setToolTipText(help);
+ ControlDecoration decoration = createFieldDecoration(id, checkBox,
+ help);
+ }
+ break;
+ }
+ case ENUM: {
+ Label label = new Label(container, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false,
+ 1, 1));
+ label.setText(name);
+
+ Combo combo = createOptionCombo(parameter, container, mValues.parameters,
+ this, this);
+ combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 2, 1));
+
+ if (mFirst == null) {
+ mFirst = combo;
+ }
+
+ if (help != null && !help.isEmpty()) {
+ ControlDecoration decoration = createFieldDecoration(id, combo, help);
+ }
+ break;
+ }
+ case SEPARATOR:
+ // Already handled above
+ assert false : type;
+ break;
+ default:
+ assert false : type;
+ }
+ }
+ }
+
+ // Preview
+ mPreview = new ImageControl(parent, SWT.NONE, null);
+ mPreview.setDisposeImage(false); // Handled manually in this class
+ GridData gd_mImage = new GridData(SWT.CENTER, SWT.CENTER, false, false, 1, 1);
+ gd_mImage.widthHint = PREVIEW_WIDTH + 2 * PREVIEW_PADDING;
+ mPreview.setLayoutData(gd_mImage);
+
+ Label separator = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+ GridData separatorData = new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1);
+ separatorData.heightHint = 16;
+ separator.setLayoutData(separatorData);
+
+ // Generic help
+ mHelpIcon = new Label(parent, SWT.NONE);
+ mHelpIcon.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1));
+ Image icon = IconFactory.getInstance().getIcon("quickfix");
+ mHelpIcon.setImage(icon);
+ mHelpIcon.setVisible(false);
+ mTipLabel = new Label(parent, SWT.WRAP);
+ mTipLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+
+ setPreview(thumb);
+
+ parent.layout(true, true);
+ // TODO: This is a workaround for the fact that (at least on OSX) you end up
+ // with some visual artifacts from the control decorations in the upper left corner
+ // (outside the parent widget itself) from the initial control decoration placement
+ // prior to layout. Therefore, perform a redraw. A better solution would be to
+ // delay creation of the control decorations until layout has been performed.
+ // Let's do that soon.
+ parent.getParent().redraw();
+ }
+
+ @NonNull
+ static Combo createOptionCombo(
+ @NonNull Parameter parameter,
+ @NonNull Composite container,
+ @NonNull Map<String, Object> valueMap,
+ @NonNull SelectionListener selectionListener,
+ @NonNull FocusListener focusListener) {
+ Combo combo = new Combo(container, SWT.READ_ONLY);
+
+ List<Element> options = parameter.getOptions();
+ assert options.size() > 0;
+ int selected = 0;
+ List<String> ids = Lists.newArrayList();
+ List<Integer> minSdks = Lists.newArrayList();
+ List<Integer> minBuildApis = Lists.newArrayList();
+ List<String> labels = Lists.newArrayList();
+ for (int i = 0, n = options.size(); i < n; i++) {
+ Element option = options.get(i);
+ String optionId = option.getAttribute(ATTR_ID);
+ assert optionId != null && !optionId.isEmpty() : ATTR_ID;
+ String isDefault = option.getAttribute(ATTR_DEFAULT);
+ if (isDefault != null && !isDefault.isEmpty() &&
+ Boolean.valueOf(isDefault)) {
+ selected = i;
+ }
+ NodeList childNodes = option.getChildNodes();
+ assert childNodes.getLength() == 1 &&
+ childNodes.item(0).getNodeType() == Node.TEXT_NODE;
+ String optionLabel = childNodes.item(0).getNodeValue().trim();
+
+ String minApiString = option.getAttribute(ATTR_MIN_API);
+ int minSdk = 1;
+ if (minApiString != null && !minApiString.isEmpty()) {
+ try {
+ minSdk = Integer.parseInt(minApiString);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should
+ // always be an integer
+ AdtPlugin.log(nufe, null);
+ minSdk = 1;
+ }
+ }
+ String minBuildApiString = option.getAttribute(ATTR_MIN_BUILD_API);
+ int minBuildApi = 1;
+ if (minBuildApiString != null && !minBuildApiString.isEmpty()) {
+ try {
+ minBuildApi = Integer.parseInt(minBuildApiString);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should
+ // always be an integer
+ AdtPlugin.log(nufe, null);
+ minBuildApi = 1;
+ }
+ }
+ minSdks.add(minSdk);
+ minBuildApis.add(minBuildApi);
+ ids.add(optionId);
+ labels.add(optionLabel);
+ }
+ combo.setData(parameter);
+ parameter.control = combo;
+ combo.setData(ATTR_ID, ids.toArray(new String[ids.size()]));
+ combo.setData(ATTR_MIN_API, minSdks.toArray(new Integer[minSdks.size()]));
+ combo.setData(ATTR_MIN_BUILD_API, minBuildApis.toArray(
+ new Integer[minBuildApis.size()]));
+ assert labels.size() > 0;
+ combo.setItems(labels.toArray(new String[labels.size()]));
+ combo.select(selected);
+
+ combo.addSelectionListener(selectionListener);
+ combo.addFocusListener(focusListener);
+
+ valueMap.put(parameter.id, ids.get(selected));
+
+ if (parameter.help != null && !parameter.help.isEmpty()) {
+ combo.setToolTipText(parameter.help);
+ }
+
+ return combo;
+ }
+
+ private void setPreview(String thumb) {
+ Image oldImage = mPreviewImage;
+ boolean dispose = mDisposePreviewImage;
+ mPreviewImage = null;
+
+ if (thumb == null || thumb.isEmpty()) {
+ mPreviewImage = TemplateMetadata.getDefaultTemplateIcon();
+ mDisposePreviewImage = false;
+ } else {
+ byte[] data = mValues.getTemplateHandler().readTemplateResource(thumb);
+ if (data != null) {
+ try {
+ mPreviewImage = new Image(getControl().getDisplay(),
+ new ByteArrayInputStream(data));
+ mDisposePreviewImage = true;
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ if (mPreviewImage == null) {
+ return;
+ }
+ }
+
+ mPreview.setImage(mPreviewImage);
+ mPreview.fitToWidth(PREVIEW_WIDTH);
+
+ if (oldImage != null && dispose) {
+ oldImage.dispose();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mPreviewImage != null && mDisposePreviewImage) {
+ mDisposePreviewImage = false;
+ mPreviewImage.dispose();
+ mPreviewImage = null;
+ }
+ }
+
+ private ControlDecoration createFieldDecoration(String id, Control control,
+ String description) {
+ ControlDecoration decoration = new ControlDecoration(control, SWT.LEFT);
+ decoration.setMarginWidth(2);
+ FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
+ getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION);
+ decoration.setImage(errorFieldIndicator.getImage());
+ decoration.setDescriptionText(description);
+ control.setToolTipText(description);
+ mDecorations.put(id, decoration);
+
+ return decoration;
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ // Force user to reach this page before hitting Finish
+ return mShown && super.isPageComplete();
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ onEnter();
+ }
+
+ super.setVisible(visible);
+
+ if (mFirst != null) {
+ mFirst.setFocus();
+ }
+
+ if (visible) {
+ mShown = true;
+ }
+
+ validatePage();
+ }
+
+ /** Returns the parameter associated with the given control */
+ @Nullable
+ static Parameter getParameter(Control control) {
+ return (Parameter) control.getData();
+ }
+
+ /**
+ * Returns the current string evaluator, if any
+ *
+ * @return the evaluator or null
+ */
+ @Nullable
+ public StringEvaluator getEvaluator() {
+ return mEvaluator;
+ }
+
+ // ---- Validation ----
+
+ private void validatePage() {
+ int minSdk = getMinSdk();
+ int buildApi = getBuildApi();
+ IStatus status = mValues.getTemplateHandler().validateTemplate(minSdk, buildApi);
+
+ if (status == null || status.isOK()) {
+ if (mChooseProject && mValues.project == null) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Please select an Android project.");
+ }
+ }
+
+ for (Parameter parameter : mShowingTemplate.getParameters()) {
+ if (parameter.type == Parameter.Type.SEPARATOR) {
+ continue;
+ }
+ IInputValidator validator = parameter.getValidator(mValues.project);
+ if (validator != null) {
+ ControlDecoration decoration = mDecorations.get(parameter.id);
+ String value = parameter.value == null ? "" : parameter.value.toString();
+ String error = validator.isValid(value);
+ if (error != null) {
+ IStatus s = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, error);
+ if (decoration != null) {
+ updateDecorator(decoration, s, parameter.help);
+ }
+ if (status == null || status.isOK()) {
+ status = s;
+ }
+ } else if (decoration != null) {
+ updateDecorator(decoration, null, parameter.help);
+ }
+ }
+
+ if (status == null || status.isOK()) {
+ if (parameter.control instanceof Combo) {
+ status = validateCombo(status, parameter, minSdk, buildApi);
+ }
+ }
+ }
+
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ /** Validates the given combo */
+ static IStatus validateCombo(IStatus status, Parameter parameter, int minSdk, int buildApi) {
+ Combo combo = (Combo) parameter.control;
+ int index = combo.getSelectionIndex();
+ return validateCombo(status, parameter, index, minSdk, buildApi);
+ }
+
+ /** Validates the given combo assuming the value at the given index is chosen */
+ static IStatus validateCombo(IStatus status, Parameter parameter, int index,
+ int minSdk, int buildApi) {
+ Combo combo = (Combo) parameter.control;
+ Integer[] optionIds = (Integer[]) combo.getData(ATTR_MIN_API);
+ // Check minSdk
+ if (index != -1 && index < optionIds.length) {
+ Integer requiredMinSdk = optionIds[index];
+ if (requiredMinSdk > minSdk) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "%1$s \"%2$s\" requires a minimum SDK version of at " +
+ "least %3$d, and the current min version is %4$d",
+ parameter.name, combo.getItems()[index], requiredMinSdk, minSdk));
+ }
+ }
+
+ // Check minimum build target
+ optionIds = (Integer[]) combo.getData(ATTR_MIN_BUILD_API);
+ if (index != -1 && index < optionIds.length) {
+ Integer requiredBuildApi = optionIds[index];
+ if (requiredBuildApi > buildApi) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "%1$s \"%2$s\" requires a build target API version of at " +
+ "least %3$d, and the current version is %4$d",
+ parameter.name, combo.getItems()[index], requiredBuildApi, buildApi));
+ }
+ }
+ return status;
+ }
+
+ private int getMinSdk() {
+ return mChooseProject ? mValues.getMinSdk() : mCustomMinSdk;
+ }
+
+ private int getBuildApi() {
+ return mChooseProject ? mValues.getBuildApi() : mCustomBuildApi;
+ }
+
+ private void updateDecorator(ControlDecoration decorator, IStatus status, String help) {
+ if (help != null && !help.isEmpty()) {
+ decorator.setDescriptionText(status != null ? status.getMessage() : help);
+
+ int severity = status != null ? status.getSeverity() : IStatus.OK;
+ String id;
+ if (severity == IStatus.ERROR) {
+ id = FieldDecorationRegistry.DEC_ERROR;
+ } else if (severity == IStatus.WARNING) {
+ id = FieldDecorationRegistry.DEC_WARNING;
+ } else {
+ id = FieldDecorationRegistry.DEC_INFORMATION;
+ }
+ FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault().
+ getFieldDecoration(id);
+ decorator.setImage(errorFieldIndicator.getImage());
+ } else {
+ if (status == null || status.isOK()) {
+ decorator.hide();
+ } else {
+ decorator.show();
+ }
+ }
+ }
+
+ // ---- Implements ModifyListener ----
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source instanceof Text) {
+ Text text = (Text) source;
+ editParameter(text, text.getText().trim());
+ }
+
+ validatePage();
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mProjectButton) {
+ mValues.project = mProjectButton.getSelectedProject();
+ } else if (source instanceof Combo) {
+ Combo combo = (Combo) source;
+ String[] optionIds = (String[]) combo.getData(ATTR_ID);
+ int index = combo.getSelectionIndex();
+ if (index != -1 && index < optionIds.length) {
+ String optionId = optionIds[index];
+ editParameter(combo, optionId);
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+ if (template != null) {
+ setPreview(template.getThumbnailPath());
+ }
+ }
+ } else if (source instanceof Button) {
+ Button button = (Button) source;
+ Parameter parameter = (Parameter) button.getData();
+ if (parameter.type == Type.BOOLEAN) {
+ // Checkbox parameter
+ editParameter(button, button.getSelection());
+
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+ if (template != null) {
+ setPreview(template.getThumbnailPath());
+ }
+ } else {
+ // Choose button for some other parameter, usually a text
+ String activity = chooseActivity();
+ if (activity != null) {
+ setValue(parameter, activity);
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ private String chooseActivity() {
+ try {
+ // Compute a search scope: We need to merge all the subclasses
+ // android.app.Fragment and android.support.v4.app.Fragment
+ IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
+ IProject project = mValues.project;
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType activityType = null;
+
+ if (javaProject != null) {
+ activityType = javaProject.findType(CLASS_ACTIVITY);
+ }
+ if (activityType == null) {
+ IJavaProject[] projects = BaseProjectHelper.getAndroidProjects(null);
+ for (IJavaProject p : projects) {
+ activityType = p.findType(CLASS_ACTIVITY);
+ if (activityType != null) {
+ break;
+ }
+ }
+ }
+ if (activityType != null) {
+ NullProgressMonitor monitor = new NullProgressMonitor();
+ ITypeHierarchy hierarchy = activityType.newTypeHierarchy(monitor);
+ IType[] classes = hierarchy.getAllSubtypes(activityType);
+ scope = SearchEngine.createJavaSearchScope(classes, IJavaSearchScope.SOURCES);
+ }
+
+ Shell parent = AdtPlugin.getShell();
+ final SelectionDialog dialog = JavaUI.createTypeDialog(
+ parent,
+ new ProgressMonitorDialog(parent),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, false,
+ // Use ? as a default filter to fill dialog with matches
+ "?", //$NON-NLS-1$
+ new TypeSelectionExtension() {
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ @Override
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+ int modifiers = typeInfoRequestor.getModifiers();
+ if (!Flags.isPublic(modifiers)
+ || Flags.isInterface(modifiers)
+ || Flags.isEnum(modifiers)) {
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+ });
+
+ dialog.setTitle("Choose Activity Class");
+ dialog.setMessage("Select an Activity class (? = any character, * = any string):");
+ if (dialog.open() == IDialogConstants.CANCEL_ID) {
+ return null;
+ }
+
+ Object[] types = dialog.getResult();
+ if (types != null && types.length > 0) {
+ return ((IType) types[0]).getFullyQualifiedName();
+ }
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return null;
+ }
+
+ private void editParameter(Control control, Object value) {
+ Parameter parameter = getParameter(control);
+ if (parameter != null) {
+ String id = parameter.id;
+ parameter.value = value;
+ parameter.edited = value != null && !value.toString().isEmpty();
+ mValues.parameters.put(id, value);
+
+ // Update dependent variables, if any
+ List<Parameter> parameters = mShowingTemplate.getParameters();
+ for (Parameter p : parameters) {
+ if (p == parameter || p.suggest == null || p.edited ||
+ p.type == Parameter.Type.SEPARATOR) {
+ continue;
+ }
+ if (!p.suggest.contains(id)) {
+ continue;
+ }
+
+ try {
+ if (mEvaluator == null) {
+ mEvaluator = new StringEvaluator();
+ }
+ String updated = mEvaluator.evaluate(p.suggest, parameters);
+ if (updated != null && !updated.equals(p.value)) {
+ setValue(p, updated);
+ }
+ } catch (Throwable t) {
+ // Pass: Ignore updating if something wrong happens
+ t.printStackTrace(); // during development only
+ }
+ }
+ }
+ }
+
+ private void setValue(Parameter p, String value) {
+ p.value = value;
+ mValues.parameters.put(p.id, value);
+
+ // Update form widgets
+ boolean prevIgnore = mIgnore;
+ try {
+ mIgnore = true;
+ if (p.control instanceof Text) {
+ ((Text) p.control).setText(value);
+ } else if (p.control instanceof Button) {
+ // TODO: Handle
+ } else if (p.control instanceof Combo) {
+ // TODO: Handle
+ } else if (p.control != null) {
+ assert false : p.control;
+ }
+ } finally {
+ mIgnore = prevIgnore;
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- Implements FocusListener ----
+
+ @Override
+ public void focusGained(FocusEvent e) {
+ Object source = e.getSource();
+ String tip = "";
+
+ if (source instanceof Control) {
+ Control control = (Control) source;
+ Parameter parameter = getParameter(control);
+ if (parameter != null) {
+ ControlDecoration decoration = mDecorations.get(parameter.id);
+ if (decoration != null) {
+ tip = decoration.getDescriptionText();
+ }
+ }
+ }
+
+ mTipLabel.setText(tip);
+ mHelpIcon.setVisible(tip.length() > 0);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mTipLabel.setText("");
+ mHelpIcon.setVisible(false);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java
new file mode 100644
index 000000000..99814f731
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API_LEVEL;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_TARGET_API;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.wizards.newresource.BasicNewResourceWizard;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Template wizard which creates parameterized templates
+ */
+public class NewTemplateWizard extends TemplateWizard {
+ /** Template name and location under $sdk/templates for the default activity */
+ static final String BLANK_ACTIVITY = "activities/BlankActivity"; //$NON-NLS-1$
+ /** Template name and location under $sdk/templates for the custom view template */
+ static final String CUSTOM_VIEW = "other/CustomView"; //$NON-NLS-1$
+
+ protected NewTemplatePage mMainPage;
+ protected NewTemplateWizardState mValues;
+ private final String mTemplateName;
+
+ NewTemplateWizard(String templateName) {
+ mTemplateName = templateName;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ mValues = new NewTemplateWizardState();
+
+ File template = TemplateManager.getTemplateLocation(mTemplateName);
+ if (template != null) {
+ mValues.setTemplateLocation(template);
+ }
+ hideBuiltinParameters();
+
+ List<IProject> projects = AdtUtils.getSelectedProjects(selection);
+ if (projects.size() == 1) {
+ mValues.project = projects.get(0);
+ }
+
+ mMainPage = new NewTemplatePage(mValues, true);
+ }
+
+ @Override
+ protected boolean shouldAddIconPage() {
+ return mValues.getIconState() != null;
+ }
+
+ /**
+ * Hide those parameters that the template requires but that we don't want
+ * to ask the users about, since we can derive it from the target project
+ * the template is written into.
+ */
+ protected void hideBuiltinParameters() {
+ Set<String> hidden = mValues.hidden;
+ hidden.add(ATTR_PACKAGE_NAME);
+ hidden.add(ATTR_MIN_API);
+ hidden.add(ATTR_MIN_API_LEVEL);
+ hidden.add(ATTR_TARGET_API);
+ hidden.add(ATTR_BUILD_API);
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mMainPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+
+ if (page == mMainPage && shouldAddIconPage()) {
+ WizardPage iconPage = getIconPage(mValues.getIconState());
+ mValues.updateIconState(mMainPage.getEvaluator());
+ return iconPage;
+ } else if (page == mMainPage
+ || shouldAddIconPage() && page == getIconPage(mValues.getIconState())) {
+ if (template != null) {
+ if (InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getPreviewPage(mValues);
+ } else {
+ return getDependencyPage(template, true);
+ }
+ }
+ } else if (page == getDependencyPage(template, false)) {
+ return getPreviewPage(mValues);
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ @NonNull
+ protected IProject getProject() {
+ return mValues.project;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ TemplateHandler activityTemplate = mValues.getTemplateHandler();
+ return activityTemplate.getFilesToOpen();
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ TemplateHandler activityTemplate = mValues.getTemplateHandler();
+ return activityTemplate.getFinalizingActions();
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ return mValues.computeChanges();
+ }
+
+ /**
+ * Opens the given set of files (as relative paths within a given project
+ *
+ * @param project the project containing the paths
+ * @param relativePaths the paths to files to open
+ * @param mWorkbench the workbench to open the files in
+ */
+ public static void openFiles(
+ @NonNull final IProject project,
+ @NonNull final List<String> relativePaths,
+ @NonNull final IWorkbench mWorkbench) {
+ if (!relativePaths.isEmpty()) {
+ // This has to be delayed in order for focus handling to work correctly
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ for (String path : relativePaths) {
+ IResource resource = project.findMember(path);
+ if (resource != null) {
+ if (resource instanceof IFile) {
+ try {
+ AdtPlugin.openFile((IFile) resource, null, false);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Failed to open %1$s", //$NON-NLS-1$
+ resource.getFullPath().toString());
+ }
+ }
+ boolean isLast = relativePaths.size() == 1 ||
+ path.equals(relativePaths.get(relativePaths.size() - 1));
+ if (isLast) {
+ BasicNewResourceWizard.selectAndReveal(resource,
+ mWorkbench.getActiveWorkbenchWindow());
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Specific New Custom View wizard
+ */
+ public static class NewCustomViewWizard extends NewTemplateWizard {
+ /** Creates a new {@link NewCustomViewWizard} */
+ public NewCustomViewWizard() {
+ super(CUSTOM_VIEW);
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+ setWindowTitle("New Custom View");
+ super.mMainPage.setTitle("New Custom View");
+ super.mMainPage.setDescription("Creates a new custom view");
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java
new file mode 100644
index 000000000..2c97003f2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_COPY_ICONS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API_LEVEL;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_TARGET_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LIBRARY_PROJECT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_NEW_PROJECT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard.BLANK_ACTIVITY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Value object which holds the current state of the wizard pages for the
+ * {@link NewTemplateWizard}
+ */
+public class NewTemplateWizardState {
+ /** Template handler responsible for instantiating templates and reading resources */
+ private TemplateHandler mTemplateHandler;
+
+ /** Configured parameters, by id */
+ public final Map<String, Object> parameters = new HashMap<String, Object>();
+
+ /** Configured defaults for the parameters, by id */
+ public final Map<String, String> defaults = new HashMap<String, String>();
+
+ /** Ids for parameters which should be hidden (because the client wizard already
+ * has information for these parameters) */
+ public final Set<String> hidden = new HashSet<String>();
+
+ /**
+ * The chosen project (which may be null if the wizard page is being
+ * embedded in the new project wizard)
+ */
+ public IProject project;
+
+ /** The minimum API level to use for this template */
+ public int minSdkLevel;
+
+ /** Location of the template being created */
+ private File mTemplateLocation;
+
+ /**
+ * State for the asset studio wizard, used to create custom icons provided
+ * the icon requests it with an {@code <icons>} element
+ */
+ private CreateAssetSetWizardState mIconState;
+
+ /**
+ * Create a new state object for use by the {@link NewTemplatePage}
+ */
+ public NewTemplateWizardState() {
+ parameters.put(IS_NEW_PROJECT, false);
+ }
+
+ @NonNull
+ TemplateHandler getTemplateHandler() {
+ if (mTemplateHandler == null) {
+ File inputPath;
+ if (mTemplateLocation != null) {
+ inputPath = mTemplateLocation;
+ } else {
+ // Default
+ inputPath = TemplateManager.getTemplateLocation(BLANK_ACTIVITY);
+ }
+ mTemplateHandler = TemplateHandler.createFromPath(inputPath);
+ }
+
+ return mTemplateHandler;
+ }
+
+ /** Sets the current template */
+ void setTemplateLocation(File file) {
+ if (!file.equals(mTemplateLocation)) {
+ mTemplateLocation = file;
+ mTemplateHandler = null;
+ }
+ }
+
+ /** Returns the current template */
+ File getTemplateLocation() {
+ return mTemplateLocation;
+ }
+
+ /** Returns the min SDK version to use */
+ int getMinSdk() {
+ if (project == null) {
+ return -1;
+ }
+ ManifestInfo manifest = ManifestInfo.get(project);
+ return manifest.getMinSdkVersion();
+ }
+
+ /** Returns the build API version to use */
+ int getBuildApi() {
+ if (project == null) {
+ return -1;
+ }
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+ if (target != null) {
+ return target.getVersion().getApiLevel();
+ }
+
+ return getMinSdk();
+ }
+
+ /** Computes the changes this wizard will make */
+ @NonNull
+ List<Change> computeChanges() {
+ if (project == null) {
+ return Collections.emptyList();
+ }
+
+ ManifestInfo manifest = ManifestInfo.get(project);
+ parameters.put(ATTR_PACKAGE_NAME, manifest.getPackage());
+ parameters.put(ATTR_MIN_API, manifest.getMinSdkName());
+ parameters.put(ATTR_MIN_API_LEVEL, manifest.getMinSdkVersion());
+ parameters.put(ATTR_TARGET_API, manifest.getTargetSdkVersion());
+ parameters.put(ATTR_BUILD_API, getBuildApi());
+ parameters.put(ATTR_COPY_ICONS, mIconState == null);
+ ProjectState projectState = Sdk.getProjectState(project);
+ parameters.put(IS_LIBRARY_PROJECT,
+ projectState != null ? projectState.isLibrary() : false);
+
+ TemplateHandler.addDirectoryParameters(parameters, project);
+
+ List<Change> changes = getTemplateHandler().render(project, parameters);
+
+ if (mIconState != null) {
+ String title = String.format("Generate icons (res/drawable-<density>/%1$s.png)",
+ mIconState.outputName);
+ changes.add(new NullChange(title) {
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ ConfigureAssetSetPage.generateIcons(mIconState.project,
+ mIconState, false, null);
+
+ // Not undoable: just return null instead of an undo-change.
+ return null;
+ }
+ });
+
+ }
+
+ return changes;
+ }
+
+ @NonNull
+ CreateAssetSetWizardState getIconState() {
+ if (mIconState == null) {
+ TemplateHandler handler = getTemplateHandler();
+ if (handler != null) {
+ TemplateMetadata template = handler.getTemplate();
+ if (template != null) {
+ mIconState = template.getIconState(project);
+ }
+ }
+ }
+
+ return mIconState;
+ }
+
+ /**
+ * Updates the icon state, such as the output name, based on other parameter settings
+ * @param evaluator the string evaluator, or null if none exists
+ */
+ public void updateIconState(@Nullable StringEvaluator evaluator) {
+ TemplateMetadata template = getTemplateHandler().getTemplate();
+ if (template != null) {
+ if (evaluator == null) {
+ evaluator = new StringEvaluator();
+ }
+ template.updateIconName(template.getParameters(), evaluator);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java
new file mode 100644
index 000000000..3139451c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CONSTRAINTS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_HELP;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_ID;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SUGGEST;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.google.common.base.Splitter;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.swt.widgets.Control;
+import org.w3c.dom.Element;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A template parameter editable and edited by the user.
+ * <p>
+ * Note that this class encapsulates not just the metadata provided by the
+ * template, but the actual editing operation of that template in the wizard: it
+ * also captures current values, a reference to the editing widget (such that
+ * related widgets can be updated when one value depends on another etc)
+ */
+class Parameter {
+ enum Type {
+ STRING,
+ BOOLEAN,
+ ENUM,
+ SEPARATOR;
+ // TODO: Numbers?
+
+ public static Type get(String name) {
+ try {
+ return Type.valueOf(name.toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ AdtPlugin.printErrorToConsole("Unexpected template type '" + name + "'");
+ AdtPlugin.printErrorToConsole("Expected one of :");
+ for (Type s : Type.values()) {
+ AdtPlugin.printErrorToConsole(" " + s.name().toLowerCase(Locale.US));
+ }
+ }
+
+ return STRING;
+ }
+ }
+
+ /**
+ * Constraints that can be applied to a parameter which helps the UI add a
+ * validator etc for user input. These are typically combined into a set
+ * of constraints via an EnumSet.
+ */
+ enum Constraint {
+ /**
+ * This value must be unique. This constraint usually only makes sense
+ * when other constraints are specified, such as {@link #LAYOUT}, which
+ * means that the parameter should designate a name that does not
+ * represent an existing layout resource name
+ */
+ UNIQUE,
+
+ /**
+ * This value must already exist. This constraint usually only makes sense
+ * when other constraints are specified, such as {@link #LAYOUT}, which
+ * means that the parameter should designate a name that already exists as
+ * a resource name.
+ */
+ EXISTS,
+
+ /** The associated value must not be empty */
+ NONEMPTY,
+
+ /** The associated value is allowed to be empty */
+ EMPTY,
+
+ /** The associated value should represent a fully qualified activity class name */
+ ACTIVITY,
+
+ /** The associated value should represent an API level */
+ APILEVEL,
+
+ /** The associated value should represent a valid class name */
+ CLASS,
+
+ /** The associated value should represent a valid package name */
+ PACKAGE,
+
+ /** The associated value should represent a valid layout resource name */
+ LAYOUT,
+
+ /** The associated value should represent a valid drawable resource name */
+ DRAWABLE,
+
+ /** The associated value should represent a valid id resource name */
+ ID,
+
+ /** The associated value should represent a valid string resource name */
+ STRING;
+
+ public static Constraint get(String name) {
+ try {
+ return Constraint.valueOf(name.toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ AdtPlugin.printErrorToConsole("Unexpected template constraint '" + name + "'");
+ if (name.indexOf(',') != -1) {
+ AdtPlugin.printErrorToConsole("Use | to separate constraints");
+ } else {
+ AdtPlugin.printErrorToConsole("Expected one of :");
+ for (Constraint s : Constraint.values()) {
+ AdtPlugin.printErrorToConsole(" " + s.name().toLowerCase(Locale.US));
+ }
+ }
+ }
+
+ return NONEMPTY;
+ }
+ }
+
+ /** The template defining the parameter */
+ public final TemplateMetadata template;
+
+ /** The type of parameter */
+ @NonNull
+ public final Type type;
+
+ /** The unique id of the parameter (not displayed to the user) */
+ @Nullable
+ public final String id;
+
+ /** The display name for this parameter */
+ @Nullable
+ public final String name;
+
+ /**
+ * The initial value for this parameter (see also {@link #suggest} for more
+ * dynamic defaults
+ */
+ @Nullable
+ public final String initial;
+
+ /**
+ * A template expression using other template parameters for producing a
+ * default value based on other edited parameters, if possible.
+ */
+ @Nullable
+ public final String suggest;
+
+ /** Help for the parameter, if any */
+ @Nullable
+ public final String help;
+
+ /** The currently edited value */
+ @Nullable
+ public Object value;
+
+ /** The control showing this value */
+ @Nullable
+ public Control control;
+
+ /** The decoration associated with the control */
+ @Nullable
+ public ControlDecoration decoration;
+
+ /** Whether the parameter has been edited */
+ public boolean edited;
+
+ /** The element defining this parameter */
+ @NonNull
+ public final Element element;
+
+ /** The constraints applicable for this parameter */
+ @NonNull
+ public final EnumSet<Constraint> constraints;
+
+ /** The validator, if any, for this field */
+ private IInputValidator mValidator;
+
+ /** True if this field has no validator */
+ private boolean mNoValidator;
+
+ /** Project associated with this validator */
+ private IProject mValidatorProject;
+
+ Parameter(@NonNull TemplateMetadata template, @NonNull Element parameter) {
+ this.template = template;
+ element = parameter;
+
+ String typeName = parameter.getAttribute(TemplateHandler.ATTR_TYPE);
+ assert typeName != null && !typeName.isEmpty() : TemplateHandler.ATTR_TYPE;
+ type = Type.get(typeName);
+
+ id = parameter.getAttribute(ATTR_ID);
+ initial = parameter.getAttribute(ATTR_DEFAULT);
+ suggest = parameter.getAttribute(ATTR_SUGGEST);
+ name = parameter.getAttribute(ATTR_NAME);
+ help = parameter.getAttribute(ATTR_HELP);
+ String constraintString = parameter.getAttribute(ATTR_CONSTRAINTS);
+ if (constraintString != null && !constraintString.isEmpty()) {
+ EnumSet<Constraint> constraintSet = null;
+ for (String s : Splitter.on('|').omitEmptyStrings().split(constraintString)) {
+ Constraint constraint = Constraint.get(s);
+ if (constraintSet == null) {
+ constraintSet = EnumSet.of(constraint);
+ } else {
+ constraintSet = EnumSet.copyOf(constraintSet);
+ constraintSet.add(constraint);
+ }
+ }
+ constraints = constraintSet;
+ } else {
+ constraints = EnumSet.noneOf(Constraint.class);
+ }
+
+ if (initial != null && !initial.isEmpty() && type == Type.BOOLEAN) {
+ value = Boolean.valueOf(initial);
+ } else {
+ value = initial;
+ }
+ }
+
+ Parameter(
+ @NonNull TemplateMetadata template,
+ @NonNull Type type,
+ @NonNull String id,
+ @NonNull String initialValue) {
+ this.template = template;
+ this.type = type;
+ this.id = id;
+ this.value = initialValue;
+ element = null;
+ initial = null;
+ suggest = null;
+ name = id;
+ help = null;
+ constraints = EnumSet.noneOf(Constraint.class);
+ }
+
+ List<Element> getOptions() {
+ if (element != null) {
+ return DomUtilities.getChildren(element);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Nullable
+ public IInputValidator getValidator(@Nullable final IProject project) {
+ if (mNoValidator) {
+ return null;
+ }
+
+ if (project != mValidatorProject) {
+ // Force update of validators if the project changes, since the validators
+ // are often tied to project metadata (for example, the resource name validators
+ // which look for name conflicts)
+ mValidator = null;
+ mValidatorProject = project;
+ }
+
+ if (mValidator == null) {
+ if (constraints.contains(Constraint.LAYOUT)) {
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ mValidator = ResourceNameValidator.create(false, project, ResourceType.LAYOUT);
+ } else {
+ mValidator = ResourceNameValidator.create(false, ResourceFolderType.LAYOUT);
+ }
+ return mValidator;
+ } else if (constraints.contains(Constraint.STRING)) {
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ mValidator = ResourceNameValidator.create(false, project, ResourceType.STRING);
+ } else {
+ mValidator = ResourceNameValidator.create(false, ResourceFolderType.VALUES);
+ }
+ return mValidator;
+ } else if (constraints.contains(Constraint.ID)) {
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ mValidator = ResourceNameValidator.create(false, project, ResourceType.ID);
+ } else {
+ mValidator = ResourceNameValidator.create(false, ResourceFolderType.VALUES);
+ }
+ return mValidator;
+ } else if (constraints.contains(Constraint.DRAWABLE)) {
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ mValidator = ResourceNameValidator.create(false, project,
+ ResourceType.DRAWABLE);
+ } else {
+ mValidator = ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE);
+ }
+ return mValidator;
+ } else if (constraints.contains(Constraint.PACKAGE)
+ || constraints.contains(Constraint.CLASS)
+ || constraints.contains(Constraint.ACTIVITY)) {
+ mValidator = new IInputValidator() {
+ @Override
+ public String isValid(String newText) {
+ newText = newText.trim();
+ if (newText.isEmpty()) {
+ if (constraints.contains(Constraint.EMPTY)) {
+ return null;
+ } else if (constraints.contains(Constraint.NONEMPTY)) {
+ return String.format("Enter a value for %1$s", name);
+ } else {
+ // Compatibility mode: older templates might not specify;
+ // in that case, accept empty
+ if (!"activityClass".equals(id)) { //$NON-NLS-1$
+ return null;
+ }
+ }
+ }
+ IStatus status;
+ if (constraints.contains(Constraint.ACTIVITY)) {
+ status = ApplicationInfoPage.validateActivity(newText);
+ } else if (constraints.contains(Constraint.PACKAGE)) {
+ status = ApplicationInfoPage.validatePackage(newText);
+ } else {
+ assert constraints.contains(Constraint.CLASS);
+ status = ApplicationInfoPage.validateClass(newText);
+ }
+ if (status != null && !status.isOK()) {
+ return status.getMessage();
+ }
+
+ // Uniqueness
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ try {
+ // Determine the package.
+ // If there is a package info
+
+ IJavaProject p = BaseProjectHelper.getJavaProject(project);
+ if (p != null) {
+ String fqcn = newText;
+ if (fqcn.indexOf('.') == -1) {
+ String pkg = null;
+ Parameter parameter = template.getParameter(
+ ATTR_PACKAGE_NAME);
+ if (parameter != null && parameter.value != null) {
+ pkg = parameter.value.toString();
+ } else {
+ pkg = ManifestInfo.get(project).getPackage();
+ }
+ fqcn = pkg.isEmpty() ? newText : pkg + '.' + newText;
+ }
+
+ IType t = p.findType(fqcn);
+ if (t != null && t.exists()) {
+ return String.format("%1$s already exists", newText);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+ };
+ return mValidator;
+ } else if (constraints.contains(Constraint.NONEMPTY)) {
+ mValidator = new IInputValidator() {
+ @Override
+ public String isValid(String newText) {
+ if (newText.trim().isEmpty()) {
+ return String.format("Enter a value for %1$s", name);
+ }
+
+ return null;
+ }
+ };
+ return mValidator;
+ }
+
+ // TODO: Handle EXISTS, APILEVEL (which is currently handled manually in the
+ // new project wizard, and never actually input by the user in a templated
+ // wizard)
+
+ mNoValidator = true;
+ }
+
+ return mValidator;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java
new file mode 100644
index 000000000..7d7881fcf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.WorkingSetGroup;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.WorkingSetHelper;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkingSet;
+
+import java.io.File;
+
+/**
+ * Second wizard page in the "New Project From Template" wizard
+ */
+public class ProjectContentsPage extends WizardPage
+ implements ModifyListener, SelectionListener, FocusListener {
+
+ private final NewProjectWizardState mValues;
+
+ private boolean mIgnore;
+ private Button mCustomIconToggle;
+ private Button mLibraryToggle;
+
+ private Button mUseDefaultLocationToggle;
+ private Label mLocationLabel;
+ private Text mLocationText;
+ private Button mChooseLocationButton;
+ private static String sLastProjectLocation = System.getProperty("user.home"); //$NON-NLS-1$
+ private Button mCreateActivityToggle;
+ private WorkingSetGroup mWorkingSetGroup;
+
+ ProjectContentsPage(NewProjectWizardState values) {
+ super("newAndroidApp"); //$NON-NLS-1$
+ mValues = values;
+ setTitle("New Android Application");
+ setDescription("Configure Project");
+
+ mWorkingSetGroup = new WorkingSetGroup();
+ setWorkingSets(new IWorkingSet[0]);
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ GridLayout gl_container = new GridLayout(4, false);
+ gl_container.horizontalSpacing = 10;
+ container.setLayout(gl_container);
+
+ mCustomIconToggle = new Button(container, SWT.CHECK);
+ mCustomIconToggle.setSelection(true);
+ mCustomIconToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mCustomIconToggle.setText("Create custom launcher icon");
+ mCustomIconToggle.setSelection(mValues.createIcon);
+ mCustomIconToggle.addSelectionListener(this);
+
+ mCreateActivityToggle = new Button(container, SWT.CHECK);
+ mCreateActivityToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false,
+ 4, 1));
+ mCreateActivityToggle.setText("Create activity");
+ mCreateActivityToggle.setSelection(mValues.createActivity);
+ mCreateActivityToggle.addSelectionListener(this);
+
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ mLibraryToggle = new Button(container, SWT.CHECK);
+ mLibraryToggle.setSelection(true);
+ mLibraryToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mLibraryToggle.setText("Mark this project as a library");
+ mLibraryToggle.setSelection(mValues.isLibrary);
+ mLibraryToggle.addSelectionListener(this);
+
+ // Blank line
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ mUseDefaultLocationToggle = new Button(container, SWT.CHECK);
+ mUseDefaultLocationToggle.setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mUseDefaultLocationToggle.setText("Create Project in Workspace");
+ mUseDefaultLocationToggle.addSelectionListener(this);
+
+ mLocationLabel = new Label(container, SWT.NONE);
+ mLocationLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ mLocationLabel.setText("Location:");
+
+ mLocationText = new Text(container, SWT.BORDER);
+ mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+ mLocationText.addModifyListener(this);
+
+ mChooseLocationButton = new Button(container, SWT.NONE);
+ mChooseLocationButton.setText("Browse...");
+ mChooseLocationButton.addSelectionListener(this);
+ mChooseLocationButton.setEnabled(false);
+ setUseCustomLocation(!mValues.useDefaultLocation);
+
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ Composite group = mWorkingSetGroup.createControl(container);
+ group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 4, 1));
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ if (visible) {
+ try {
+ mIgnore = true;
+ mUseDefaultLocationToggle.setSelection(mValues.useDefaultLocation);
+ mLocationText.setText(mValues.projectLocation);
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ validatePage();
+ }
+
+ private void setUseCustomLocation(boolean en) {
+ mValues.useDefaultLocation = !en;
+ mUseDefaultLocationToggle.setSelection(!en);
+ if (!en) {
+ updateProjectLocation(mValues.projectName);
+ }
+
+ mLocationLabel.setEnabled(en);
+ mLocationText.setEnabled(en);
+ mChooseLocationButton.setEnabled(en);
+ }
+
+ void init(IStructuredSelection selection, IWorkbenchPart activePart) {
+ setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
+ }
+
+ /**
+ * Returns the working sets to which the new project should be added.
+ *
+ * @return the selected working sets to which the new project should be added
+ */
+ private IWorkingSet[] getWorkingSets() {
+ return mWorkingSetGroup.getSelectedWorkingSets();
+ }
+
+ /**
+ * Sets the working sets to which the new project should be added.
+ *
+ * @param workingSets the initial selected working sets
+ */
+ private void setWorkingSets(IWorkingSet[] workingSets) {
+ assert workingSets != null;
+ mWorkingSetGroup.setWorkingSets(workingSets);
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ // Sync working set data to the value object, since the WorkingSetGroup
+ // doesn't let us add listeners to do this lazily
+ mValues.workingSets = getWorkingSets();
+
+ return super.getNextPage();
+ }
+
+ // ---- Implements ModifyListener ----
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mLocationText) {
+ mValues.projectLocation = mLocationText.getText().trim();
+ }
+
+ validatePage();
+ }
+
+
+ /** If the project should be created in the workspace, then update the project location
+ * based on the project name. */
+ private void updateProjectLocation(String projectName) {
+ if (projectName == null) {
+ projectName = "";
+ }
+
+ boolean useDefaultLocation = mUseDefaultLocationToggle.getSelection();
+
+ if (useDefaultLocation) {
+ IPath workspace = Platform.getLocation();
+ String projectLocation = workspace.append(projectName).toOSString();
+ mLocationText.setText(projectLocation);
+ mValues.projectLocation = projectLocation;
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mCustomIconToggle) {
+ mValues.createIcon = mCustomIconToggle.getSelection();
+ } else if (source == mLibraryToggle) {
+ mValues.isLibrary = mLibraryToggle.getSelection();
+ } else if (source == mCreateActivityToggle) {
+ mValues.createActivity = mCreateActivityToggle.getSelection();
+ } else if (source == mUseDefaultLocationToggle) {
+ boolean useDefault = mUseDefaultLocationToggle.getSelection();
+ setUseCustomLocation(!useDefault);
+ } else if (source == mChooseLocationButton) {
+ String dir = promptUserForLocation(getShell());
+ if (dir != null) {
+ mLocationText.setText(dir);
+ mValues.projectLocation = dir;
+ }
+ }
+
+ validatePage();
+ }
+
+ private String promptUserForLocation(Shell shell) {
+ DirectoryDialog dd = new DirectoryDialog(getShell());
+ dd.setMessage("Select folder where project should be created");
+
+ String curLocation = mLocationText.getText().trim();
+ if (!curLocation.isEmpty()) {
+ dd.setFilterPath(curLocation);
+ } else if (sLastProjectLocation != null) {
+ dd.setFilterPath(sLastProjectLocation);
+ }
+
+ String dir = dd.open();
+ if (dir != null) {
+ sLastProjectLocation = dir;
+ }
+
+ return dir;
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- Implements FocusListener ----
+
+ @Override
+ public void focusGained(FocusEvent e) {
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ }
+
+ // Validation
+
+ void validatePage() {
+ IStatus status = validateProjectLocation();
+
+ setPageComplete(status == null || status.getSeverity() != IStatus.ERROR);
+ if (status != null) {
+ setMessage(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR
+ ? IMessageProvider.ERROR : IMessageProvider.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+ static IStatus validateLocationInWorkspace(NewProjectWizardState values) {
+ if (values.useDefaultLocation) {
+ return null;
+ }
+
+ // Validate location
+ if (values.projectName != null) {
+ File dest = Platform.getLocation().append(values.projectName).toFile();
+ if (dest.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
+ "There is already a file or directory named \"%1$s\" in the selected location.",
+ values.projectName));
+ }
+ }
+
+ return null;
+ }
+
+ private IStatus validateProjectLocation() {
+ if (mValues.useDefaultLocation) {
+ return validateLocationInWorkspace(mValues);
+ }
+
+ String location = mLocationText.getText();
+ if (location.trim().isEmpty()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Provide a valid file system location where the project should be created.");
+ }
+
+ File f = new File(location);
+ if (f.exists()) {
+ if (!f.isDirectory()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not a valid folder.", location));
+ }
+
+ File[] children = f.listFiles();
+ if (children != null && children.length > 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Folder '%s' is not empty.", location));
+ }
+ }
+
+ // if the folder doesn't exist, then make sure that the parent
+ // exists and is a writable folder
+ File parent = f.getParentFile();
+ if (!parent.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Folder '%s' does not exist.", parent.getName()));
+ }
+
+ if (!parent.isDirectory()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not a folder.", parent.getName()));
+ }
+
+ if (!parent.canWrite()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not writeable.", parent.getName()));
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java
new file mode 100644
index 000000000..c1c8073c0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A template handler which can evaluate simple strings. Used to evaluate
+ * parameter constraints during UI wizard value editing.
+ * <p>
+ * Unlike the more general {@link TemplateHandler} which is used to instantiate
+ * full template files (from resources, merging into existing files etc) this
+ * evaluator supports only simple strings, referencing only values from the
+ * provided map (and builtin functions).
+ */
+class StringEvaluator implements TemplateLoader {
+ private Map<String, Object> mParameters;
+ private Configuration mFreemarker;
+ private String mCurrentExpression;
+
+ StringEvaluator() {
+ mParameters = TemplateHandler.createBuiltinMap();
+
+ mFreemarker = new Configuration();
+ mFreemarker.setObjectWrapper(new DefaultObjectWrapper());
+ mFreemarker.setTemplateLoader(this);
+ }
+
+ /** Evaluates the given expression, with the given set of parameters */
+ @Nullable
+ String evaluate(@NonNull String expression, @NonNull List<Parameter> parameters) {
+ // Render the instruction list template.
+ for (Parameter parameter : parameters) {
+ mParameters.put(parameter.id, parameter.value);
+ }
+ try {
+ mCurrentExpression = expression;
+ Template inputsTemplate = mFreemarker.getTemplate(expression);
+ StringWriter out = new StringWriter();
+ inputsTemplate.process(mParameters, out);
+ out.flush();
+ return out.toString();
+ } catch (Exception e) {
+ if (assertionsEnabled()) {
+ AdtPlugin.log(e, null);
+ }
+ return null;
+ }
+ }
+
+ // ---- Implements TemplateLoader ----
+
+ @Override
+ public Object findTemplateSource(String name) throws IOException {
+ return mCurrentExpression;
+ }
+
+ @Override
+ public long getLastModified(Object templateSource) {
+ return 0;
+ }
+
+ @Override
+ public Reader getReader(Object templateSource, String encoding) throws IOException {
+ return new StringReader(mCurrentExpression);
+ }
+
+ @Override
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
new file mode 100644
index 000000000..8e11841b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
@@ -0,0 +1,1239 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.SdkConstants.ATTR_PACKAGE;
+import static com.android.SdkConstants.DOT_AIDL;
+import static com.android.SdkConstants.DOT_FTL;
+import static com.android.SdkConstants.DOT_JAVA;
+import static com.android.SdkConstants.DOT_RS;
+import static com.android.SdkConstants.DOT_SVG;
+import static com.android.SdkConstants.DOT_TXT;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_NATIVE_LIBS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
+import com.android.manifmerger.ManifestMerger;
+import com.android.manifmerger.MergerLog;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.SdkUtils;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.ToolFactory;
+import org.eclipse.jdt.core.formatter.CodeFormatter;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.swt.SWT;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Handler which manages instantiating FreeMarker templates, copying resources
+ * and merging into existing files
+ */
+class TemplateHandler {
+ /** Highest supported format; templates with a higher number will be skipped
+ * <p>
+ * <ul>
+ * <li> 1: Initial format, supported by ADT 20 and up.
+ * <li> 2: ADT 21 and up. Boolean variables that have a default value and are not
+ * edited by the user would end up as strings in ADT 20; now they are always
+ * proper Booleans. Templates which rely on this should specify format >= 2.
+ * <li> 3: The wizard infrastructure passes the {@code isNewProject} boolean variable
+ * to indicate whether a wizard is created as part of a new blank project
+ * <li> 4: The templates now specify dependencies in the recipe file.
+ * </ul>
+ */
+ static final int CURRENT_FORMAT = 4;
+
+ /**
+ * Special marker indicating that this path refers to the special shared
+ * resource directory rather than being somewhere inside the root/ directory
+ * where all template specific resources are found
+ */
+ private static final String VALUE_TEMPLATE_DIR = "$TEMPLATEDIR"; //$NON-NLS-1$
+
+ /**
+ * Directory within the template which contains the resources referenced
+ * from the template.xml file
+ */
+ private static final String DATA_ROOT = "root"; //$NON-NLS-1$
+
+ /**
+ * Shared resource directory containing common resources shared among
+ * multiple templates
+ */
+ private static final String RESOURCE_ROOT = "resources"; //$NON-NLS-1$
+
+ /** Reserved filename which describes each template */
+ static final String TEMPLATE_XML = "template.xml"; //$NON-NLS-1$
+
+ // Various tags and attributes used in the template metadata files - template.xml,
+ // globals.xml.ftl, recipe.xml.ftl, etc.
+
+ static final String TAG_MERGE = "merge"; //$NON-NLS-1$
+ static final String TAG_EXECUTE = "execute"; //$NON-NLS-1$
+ static final String TAG_GLOBALS = "globals"; //$NON-NLS-1$
+ static final String TAG_GLOBAL = "global"; //$NON-NLS-1$
+ static final String TAG_PARAMETER = "parameter"; //$NON-NLS-1$
+ static final String TAG_COPY = "copy"; //$NON-NLS-1$
+ static final String TAG_INSTANTIATE = "instantiate"; //$NON-NLS-1$
+ static final String TAG_OPEN = "open"; //$NON-NLS-1$
+ static final String TAG_THUMB = "thumb"; //$NON-NLS-1$
+ static final String TAG_THUMBS = "thumbs"; //$NON-NLS-1$
+ static final String TAG_DEPENDENCY = "dependency"; //$NON-NLS-1$
+ static final String TAG_ICONS = "icons"; //$NON-NLS-1$
+ static final String TAG_FORMFACTOR = "formfactor"; //$NON-NLS-1$
+ static final String TAG_CATEGORY = "category"; //$NON-NLS-1$
+ static final String ATTR_FORMAT = "format"; //$NON-NLS-1$
+ static final String ATTR_REVISION = "revision"; //$NON-NLS-1$
+ static final String ATTR_VALUE = "value"; //$NON-NLS-1$
+ static final String ATTR_DEFAULT = "default"; //$NON-NLS-1$
+ static final String ATTR_SUGGEST = "suggest"; //$NON-NLS-1$
+ static final String ATTR_ID = "id"; //$NON-NLS-1$
+ static final String ATTR_NAME = "name"; //$NON-NLS-1$
+ static final String ATTR_DESCRIPTION = "description";//$NON-NLS-1$
+ static final String ATTR_TYPE = "type"; //$NON-NLS-1$
+ static final String ATTR_HELP = "help"; //$NON-NLS-1$
+ static final String ATTR_FILE = "file"; //$NON-NLS-1$
+ static final String ATTR_TO = "to"; //$NON-NLS-1$
+ static final String ATTR_FROM = "from"; //$NON-NLS-1$
+ static final String ATTR_CONSTRAINTS = "constraints";//$NON-NLS-1$
+ static final String ATTR_BACKGROUND = "background"; //$NON-NLS-1$
+ static final String ATTR_FOREGROUND = "foreground"; //$NON-NLS-1$
+ static final String ATTR_SHAPE = "shape"; //$NON-NLS-1$
+ static final String ATTR_TRIM = "trim"; //$NON-NLS-1$
+ static final String ATTR_PADDING = "padding"; //$NON-NLS-1$
+ static final String ATTR_SOURCE_TYPE = "source"; //$NON-NLS-1$
+ static final String ATTR_CLIPART_NAME = "clipartName";//$NON-NLS-1$
+ static final String ATTR_TEXT = "text"; //$NON-NLS-1$
+ static final String ATTR_SRC_DIR = "srcDir"; //$NON-NLS-1$
+ static final String ATTR_SRC_OUT = "srcOut"; //$NON-NLS-1$
+ static final String ATTR_RES_DIR = "resDir"; //$NON-NLS-1$
+ static final String ATTR_RES_OUT = "resOut"; //$NON-NLS-1$
+ static final String ATTR_MANIFEST_DIR = "manifestDir";//$NON-NLS-1$
+ static final String ATTR_MANIFEST_OUT = "manifestOut";//$NON-NLS-1$
+ static final String ATTR_PROJECT_DIR = "projectDir"; //$NON-NLS-1$
+ static final String ATTR_PROJECT_OUT = "projectOut"; //$NON-NLS-1$
+ static final String ATTR_MAVEN_URL = "mavenUrl"; //$NON-NLS-1$
+ static final String ATTR_DEBUG_KEYSTORE_SHA1 =
+ "debugKeystoreSha1"; //$NON-NLS-1$
+
+ static final String CATEGORY_ACTIVITIES = "activities";//$NON-NLS-1$
+ static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$
+ static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$
+
+ static final String MAVEN_SUPPORT_V4 = "support-v4"; //$NON-NLS-1$
+ static final String MAVEN_SUPPORT_V13 = "support-v13"; //$NON-NLS-1$
+ static final String MAVEN_APPCOMPAT = "appcompat-v7"; //$NON-NLS-1$
+
+ /** Default padding to apply in wizards around the thumbnail preview images */
+ static final int PREVIEW_PADDING = 10;
+
+ /** Default width to scale thumbnail preview images in wizards to */
+ static final int PREVIEW_WIDTH = 200;
+
+ /**
+ * List of files to open after the wizard has been created (these are
+ * identified by {@link #TAG_OPEN} elements in the recipe file
+ */
+ private final List<String> mOpen = Lists.newArrayList();
+
+ /**
+ * List of actions to perform after the wizard has finished.
+ */
+ protected List<Runnable> mFinalizingActions = Lists.newArrayList();
+
+ /** Path to the directory containing the templates */
+ @NonNull
+ private final File mRootPath;
+
+ /** The changes being processed by the template handler */
+ private List<Change> mMergeChanges;
+ private List<Change> mTextChanges;
+ private List<Change> mOtherChanges;
+
+ /** The project to write the template into */
+ private IProject mProject;
+
+ /** The template loader which is responsible for finding (and sharing) template files */
+ private final MyTemplateLoader mLoader;
+
+ /** Agree to all file-overwrites from now on? */
+ private boolean mYesToAll = false;
+
+ /** Is writing the template cancelled? */
+ private boolean mNoToAll = false;
+
+ /**
+ * Should files that we merge contents into be backed up? If yes, will
+ * create emacs-style tilde-file backups (filename.xml~)
+ */
+ private boolean mBackupMergedFiles = true;
+
+ /**
+ * Template metadata
+ */
+ private TemplateMetadata mTemplate;
+
+ private final TemplateManager mManager;
+
+ /** Creates a new {@link TemplateHandler} for the given root path */
+ static TemplateHandler createFromPath(File rootPath) {
+ return new TemplateHandler(rootPath, new TemplateManager());
+ }
+
+ /** Creates a new {@link TemplateHandler} for the template name, which should
+ * be relative to the templates directory */
+ static TemplateHandler createFromName(String category, String name) {
+ TemplateManager manager = new TemplateManager();
+
+ // Use the TemplateManager iteration which should merge contents between the
+ // extras/templates/ and tools/templates folders and pick the most recent version
+ List<File> templates = manager.getTemplates(category);
+ for (File file : templates) {
+ if (file.getName().equals(name) && category.equals(file.getParentFile().getName())) {
+ return new TemplateHandler(file, manager);
+ }
+ }
+
+ return new TemplateHandler(new File(getTemplateRootFolder(),
+ category + File.separator + name), manager);
+ }
+
+ private TemplateHandler(File rootPath, TemplateManager manager) {
+ mRootPath = rootPath;
+ mManager = manager;
+ mLoader = new MyTemplateLoader();
+ mLoader.setPrefix(mRootPath.getPath());
+ }
+
+ public TemplateManager getManager() {
+ return mManager;
+ }
+
+ public void setBackupMergedFiles(boolean backupMergedFiles) {
+ mBackupMergedFiles = backupMergedFiles;
+ }
+
+ @NonNull
+ public List<Change> render(IProject project, Map<String, Object> args) {
+ mOpen.clear();
+
+ mProject = project;
+ mMergeChanges = new ArrayList<Change>();
+ mTextChanges = new ArrayList<Change>();
+ mOtherChanges = new ArrayList<Change>();
+
+ // Render the instruction list template.
+ Map<String, Object> paramMap = createParameterMap(args);
+ Configuration freemarker = new Configuration();
+ freemarker.setObjectWrapper(new DefaultObjectWrapper());
+ freemarker.setTemplateLoader(mLoader);
+
+ processVariables(freemarker, TEMPLATE_XML, paramMap);
+
+ // Add the changes in the order where merges are shown first, then text files,
+ // and finally other files (like jars and icons which don't have previews).
+ List<Change> changes = new ArrayList<Change>();
+ changes.addAll(mMergeChanges);
+ changes.addAll(mTextChanges);
+ changes.addAll(mOtherChanges);
+ return changes;
+ }
+
+ Map<String, Object> createParameterMap(Map<String, Object> args) {
+ final Map<String, Object> paramMap = createBuiltinMap();
+
+ // Wizard parameters supplied by user, specific to this template
+ paramMap.putAll(args);
+
+ return paramMap;
+ }
+
+ /** Data model for the templates */
+ static Map<String, Object> createBuiltinMap() {
+ // Create the data model.
+ final Map<String, Object> paramMap = new HashMap<String, Object>();
+
+ // Builtin conversion methods
+ paramMap.put("slashedPackageName", new FmSlashedPackageNameMethod()); //$NON-NLS-1$
+ paramMap.put("camelCaseToUnderscore", new FmCamelCaseToUnderscoreMethod()); //$NON-NLS-1$
+ paramMap.put("underscoreToCamelCase", new FmUnderscoreToCamelCaseMethod()); //$NON-NLS-1$
+ paramMap.put("activityToLayout", new FmActivityToLayoutMethod()); //$NON-NLS-1$
+ paramMap.put("layoutToActivity", new FmLayoutToActivityMethod()); //$NON-NLS-1$
+ paramMap.put("classToResource", new FmClassNameToResourceMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlAttribute", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlText", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("extractLetters", new FmExtractLettersMethod()); //$NON-NLS-1$
+
+ // This should be handled better: perhaps declared "required packages" as part of the
+ // inputs? (It would be better if we could conditionally disable template based
+ // on availability)
+ Map<String, String> builtin = new HashMap<String, String>();
+ builtin.put("templatesRes", VALUE_TEMPLATE_DIR); //$NON-NLS-1$
+ paramMap.put("android", builtin); //$NON-NLS-1$
+
+ return paramMap;
+ }
+
+ static void addDirectoryParameters(Map<String, Object> parameters, IProject project) {
+ IPath srcDir = project.getFile(SdkConstants.SRC_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_SRC_DIR, srcDir.toString());
+
+ IPath resDir = project.getFile(SdkConstants.RES_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_RES_DIR, resDir.toString());
+
+ IPath manifestDir = project.getProjectRelativePath();
+ parameters.put(ATTR_MANIFEST_DIR, manifestDir.toString());
+ parameters.put(ATTR_MANIFEST_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_PROJECT_DIR, manifestDir.toString());
+ parameters.put(ATTR_PROJECT_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_DEBUG_KEYSTORE_SHA1, "");
+ }
+
+ @Nullable
+ public TemplateMetadata getTemplate() {
+ if (mTemplate == null) {
+ mTemplate = mManager.getTemplate(mRootPath);
+ }
+
+ return mTemplate;
+ }
+
+ @NonNull
+ public String getResourcePath(String templateName) {
+ return new File(mRootPath.getPath(), templateName).getPath();
+ }
+
+ /**
+ * Load a text resource for the given relative path within the template
+ *
+ * @param relativePath relative path within the template
+ * @return the string contents of the template text file
+ */
+ @Nullable
+ public String readTemplateTextResource(@NonNull String relativePath) {
+ try {
+ return Files.toString(new File(mRootPath,
+ relativePath.replace('/', File.separatorChar)), Charsets.UTF_8);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ @Nullable
+ public String readTemplateTextResource(@NonNull File file) {
+ assert file.isAbsolute();
+ try {
+ return Files.toString(file, Charsets.UTF_8);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ /**
+ * Reads the contents of a resource
+ *
+ * @param relativePath the path relative to the template directory
+ * @return the binary data read from the file
+ */
+ @Nullable
+ public byte[] readTemplateResource(@NonNull String relativePath) {
+ try {
+ return Files.toByteArray(new File(mRootPath, relativePath));
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ /**
+ * Most recent thrown exception during template instantiation. This should
+ * basically always be null. Used by unit tests to see if any template
+ * instantiation recorded a failure.
+ */
+ @VisibleForTesting
+ public static Exception sMostRecentException;
+
+ /** Read the given FreeMarker file and process the variable definitions */
+ private void processVariables(final Configuration freemarker,
+ String file, final Map<String, Object> paramMap) {
+ try {
+ String xml;
+ if (file.endsWith(DOT_XML)) {
+ // Just read the file
+ xml = readTemplateTextResource(file);
+ if (xml == null) {
+ return;
+ }
+ } else {
+ mLoader.setTemplateFile(new File(mRootPath, file));
+ Template inputsTemplate = freemarker.getTemplate(file);
+ StringWriter out = new StringWriter();
+ inputsTemplate.process(paramMap, out);
+ out.flush();
+ xml = out.toString();
+ }
+
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser saxParser = factory.newSAXParser();
+ saxParser.parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String name,
+ Attributes attributes)
+ throws SAXException {
+ if (TAG_PARAMETER.equals(name)) {
+ String id = attributes.getValue(ATTR_ID);
+ if (!paramMap.containsKey(id)) {
+ String value = attributes.getValue(ATTR_DEFAULT);
+ Object mapValue = value;
+ if (value != null && !value.isEmpty()) {
+ String type = attributes.getValue(ATTR_TYPE);
+ if ("boolean".equals(type)) { //$NON-NLS-1$
+ mapValue = Boolean.valueOf(value);
+ }
+ }
+ paramMap.put(id, mapValue);
+ }
+ } else if (TAG_GLOBAL.equals(name)) {
+ String id = attributes.getValue(ATTR_ID);
+ if (!paramMap.containsKey(id)) {
+ paramMap.put(id, TypedVariable.parseGlobal(attributes));
+ }
+ } else if (TAG_GLOBALS.equals(name)) {
+ // Handle evaluation of variables
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ processVariables(freemarker, path, paramMap);
+ } // else: <globals> root element
+ } else if (TAG_EXECUTE.equals(name)) {
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ execute(freemarker, path, paramMap);
+ }
+ } else if (TAG_DEPENDENCY.equals(name)) {
+ String dependencyName = attributes.getValue(ATTR_NAME);
+ if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ File path = AddSupportJarAction.getSupportJarFile();
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ }
+ } else if (!name.equals("template") && !name.equals(TAG_CATEGORY) &&
+ !name.equals(TAG_FORMFACTOR) && !name.equals("option") &&
+ !name.equals(TAG_THUMBS) && !name.equals(TAG_THUMB) &&
+ !name.equals(TAG_ICONS)) {
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ }
+ });
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private boolean canOverwrite(File file) {
+ if (file.exists()) {
+ // Warn that the file already exists and ask the user what to do
+ if (!mYesToAll) {
+ MessageDialog dialog = new MessageDialog(null, "File Already Exists", null,
+ String.format(
+ "%1$s already exists.\nWould you like to replace it?",
+ file.getPath()),
+ MessageDialog.QUESTION, new String[] {
+ // Yes will be moved to the end because it's the default
+ "Yes", "No", "Cancel", "Yes to All"
+ }, 0);
+ int result = dialog.open();
+ switch (result) {
+ case 0:
+ // Yes
+ break;
+ case 3:
+ // Yes to all
+ mYesToAll = true;
+ break;
+ case 1:
+ // No
+ return false;
+ case SWT.DEFAULT:
+ case 2:
+ // Cancel
+ mNoToAll = true;
+ return false;
+ }
+ }
+
+ if (mBackupMergedFiles) {
+ return makeBackup(file);
+ } else {
+ return file.delete();
+ }
+ }
+
+ return true;
+ }
+
+ /** Executes the given recipe file: copying, merging, instantiating, opening files etc */
+ private void execute(
+ final Configuration freemarker,
+ String file,
+ final Map<String, Object> paramMap) {
+ try {
+ mLoader.setTemplateFile(new File(mRootPath, file));
+ Template freemarkerTemplate = freemarker.getTemplate(file);
+
+ StringWriter out = new StringWriter();
+ freemarkerTemplate.process(paramMap, out);
+ out.flush();
+ String xml = out.toString();
+
+ // Parse and execute the resulting instruction list.
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ SAXParser saxParser = factory.newSAXParser();
+
+ saxParser.parse(new ByteArrayInputStream(xml.getBytes()),
+ new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String name,
+ Attributes attributes)
+ throws SAXException {
+ if (mNoToAll) {
+ return;
+ }
+
+ try {
+ boolean instantiate = TAG_INSTANTIATE.equals(name);
+ if (TAG_COPY.equals(name) || instantiate) {
+ String fromPath = attributes.getValue(ATTR_FROM);
+ String toPath = attributes.getValue(ATTR_TO);
+ if (toPath == null || toPath.isEmpty()) {
+ toPath = attributes.getValue(ATTR_FROM);
+ toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
+ }
+ IPath to = getTargetPath(toPath);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, fromPath, to);
+ } else {
+ copyTemplateResource(fromPath, to);
+ }
+ } else if (TAG_MERGE.equals(name)) {
+ String fromPath = attributes.getValue(ATTR_FROM);
+ String toPath = attributes.getValue(ATTR_TO);
+ if (toPath == null || toPath.isEmpty()) {
+ toPath = attributes.getValue(ATTR_FROM);
+ toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
+ }
+ // Resources in template.xml are located within root/
+ IPath to = getTargetPath(toPath);
+ merge(freemarker, paramMap, fromPath, to);
+ } else if (name.equals(TAG_OPEN)) {
+ // The relative path here is within the output directory:
+ String relativePath = attributes.getValue(ATTR_FILE);
+ if (relativePath != null && !relativePath.isEmpty()) {
+ mOpen.add(relativePath);
+ }
+ } else if (TAG_DEPENDENCY.equals(name)) {
+ String dependencyUrl = attributes.getValue(ATTR_MAVEN_URL);
+ File path;
+ if (dependencyUrl.contains(MAVEN_SUPPORT_V4)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ path = AddSupportJarAction.getSupportJarFile();
+ } else if (dependencyUrl.contains(MAVEN_SUPPORT_V13)) {
+ path = AddSupportJarAction.getSupport13JarFile();
+ } else if (dependencyUrl.contains(MAVEN_APPCOMPAT)) {
+ path = null;
+ mFinalizingActions.add(new Runnable() {
+ @Override
+ public void run() {
+ AddSupportJarAction.installAppCompatLibrary(mProject, true);
+ }
+ });
+ } else {
+ path = null;
+ System.err.println("WARNING: Unknown dependency type");
+ }
+
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ } else if (!name.equals("recipe") && !name.equals(TAG_DEPENDENCY)) { //$NON-NLS-1$
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+ });
+
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @NonNull
+ private File getFullPath(@NonNull String fromPath) {
+ if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) {
+ return new File(getTemplateRootFolder(), RESOURCE_ROOT + File.separator
+ + fromPath.substring(VALUE_TEMPLATE_DIR.length() + 1).replace('/',
+ File.separatorChar));
+ }
+ return new File(mRootPath, DATA_ROOT + File.separator + fromPath);
+ }
+
+ @NonNull
+ private IPath getTargetPath(@NonNull String relative) {
+ if (relative.indexOf('\\') != -1) {
+ relative = relative.replace('\\', '/');
+ }
+ return new Path(relative);
+ }
+
+ @NonNull
+ private IFile getTargetFile(@NonNull IPath path) {
+ return mProject.getFile(path);
+ }
+
+ private void merge(
+ @NonNull final Configuration freemarker,
+ @NonNull final Map<String, Object> paramMap,
+ @NonNull String relativeFrom,
+ @NonNull IPath toPath) throws IOException, TemplateException {
+
+ String currentXml = null;
+
+ IFile to = getTargetFile(toPath);
+ if (to.exists()) {
+ currentXml = AdtPlugin.readFile(to);
+ }
+
+ if (currentXml == null) {
+ // The target file doesn't exist: don't merge, just copy
+ boolean instantiate = relativeFrom.endsWith(DOT_FTL);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, relativeFrom, toPath);
+ } else {
+ copyTemplateResource(relativeFrom, toPath);
+ }
+ return;
+ }
+
+ if (!to.getFileExtension().equals(EXT_XML)) {
+ throw new RuntimeException("Only XML files can be merged at this point: " + to);
+ }
+
+ String xml = null;
+ File from = getFullPath(relativeFrom);
+ if (relativeFrom.endsWith(DOT_FTL)) {
+ // Perform template substitution of the template prior to merging
+ mLoader.setTemplateFile(from);
+ Template template = freemarker.getTemplate(from.getName());
+ Writer out = new StringWriter();
+ template.process(paramMap, out);
+ out.flush();
+ xml = out.toString();
+ } else {
+ xml = readTemplateTextResource(from);
+ if (xml == null) {
+ return;
+ }
+ }
+
+ Document currentDocument = DomUtilities.parseStructuredDocument(currentXml);
+ assert currentDocument != null : currentXml;
+ Document fragment = DomUtilities.parseStructuredDocument(xml);
+ assert fragment != null : xml;
+
+ XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST;
+ boolean modified;
+ boolean ok;
+ String fileName = to.getName();
+ if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
+ modified = ok = mergeManifest(currentDocument, fragment);
+ } else {
+ // Merge plain XML files
+ String parentFolderName = to.getParent().getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName);
+ if (folderType != null) {
+ formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath);
+ } else {
+ formatStyle = XmlFormatStyle.FILE;
+ }
+
+ modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap);
+ ok = true;
+ }
+
+ // Finally write out the merged file (formatting etc)
+ String contents = null;
+ if (ok) {
+ if (modified) {
+ contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument,
+ EclipseXmlFormatPreferences.create(), formatStyle, null,
+ currentXml.endsWith("\n")); //$NON-NLS-1$
+ }
+ } else {
+ // Just insert into file along with comment, using the "standard" conflict
+ // syntax that many tools and editors recognize.
+ String sep = SdkUtils.getLineSeparator();
+ contents =
+ "<<<<<<< Original" + sep
+ + currentXml + sep
+ + "=======" + sep
+ + xml
+ + ">>>>>>> Added" + sep;
+ }
+
+ if (contents != null) {
+ TextFileChange change = new TextFileChange("Merge " + fileName, to);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new ReplaceEdit(0, currentXml.length(), contents));
+ change.setEdit(rootEdit);
+ change.setTextType(SdkConstants.EXT_XML);
+ mMergeChanges.add(change);
+ }
+ }
+
+ /** Merges the given resource file contents into the given resource file
+ * @param paramMap */
+ private static boolean mergeResourceFile(Document currentDocument, Document fragment,
+ ResourceFolderType folderType, Map<String, Object> paramMap) {
+ boolean modified = false;
+
+ // Copy namespace declarations
+ NamedNodeMap attributes = fragment.getDocumentElement().getAttributes();
+ if (attributes != null) {
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ if (attribute.getName().startsWith(XMLNS_PREFIX)) {
+ currentDocument.getDocumentElement().setAttribute(attribute.getName(),
+ attribute.getValue());
+ }
+ }
+ }
+
+ // For layouts for example, I want to *append* inside the root all the
+ // contents of the new file.
+ // But for resources for example, I want to combine elements which specify
+ // the same name or id attribute.
+ // For elements like manifest files we need to insert stuff at the right
+ // location in a nested way (activities in the application element etc)
+ // but that doesn't happen for the other file types.
+ Element root = fragment.getDocumentElement();
+ NodeList children = root.getChildNodes();
+ List<Node> nodes = new ArrayList<Node>(children.getLength());
+ for (int i = children.getLength() - 1; i >= 0; i--) {
+ Node child = children.item(i);
+ nodes.add(child);
+ root.removeChild(child);
+ }
+ Collections.reverse(nodes);
+
+ root = currentDocument.getDocumentElement();
+
+ if (folderType == ResourceFolderType.VALUES) {
+ // Try to merge items of the same name
+ Map<String, Node> old = new HashMap<String, Node>();
+ NodeList newSiblings = root.getChildNodes();
+ for (int i = newSiblings.getLength() - 1; i >= 0; i--) {
+ Node child = newSiblings.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) child;
+ String name = getResourceId(element);
+ if (name != null) {
+ old.put(name, element);
+ }
+ }
+ }
+
+ for (Node node : nodes) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String name = getResourceId(element);
+ Node replace = name != null ? old.get(name) : null;
+ if (replace != null) {
+ // There is an existing item with the same id: just replace it
+ // ACTUALLY -- let's NOT change it.
+ // Let's say you've used the activity wizard once, and it
+ // emits some configuration parameter as a resource that
+ // it depends on, say "padding". Then the user goes and
+ // tweaks the padding to some other number.
+ // Now running the wizard a *second* time for some new activity,
+ // we should NOT go and set the value back to the template's
+ // default!
+ //root.replaceChild(node, replace);
+
+ // ... ON THE OTHER HAND... What if it's a parameter class
+ // (where the template rewrites a common attribute). Here it's
+ // really confusing if the new parameter is not set. This is
+ // really an error in the template, since we shouldn't have conflicts
+ // like that, but we need to do something to help track this down.
+ AdtPlugin.log(null,
+ "Warning: Ignoring name conflict in resource file for name %1$s",
+ name);
+ } else {
+ root.appendChild(node);
+ modified = true;
+ }
+ }
+ }
+ } else {
+ // In other file types, such as layouts, just append all the new content
+ // at the end.
+ for (Node node : nodes) {
+ root.appendChild(node);
+ modified = true;
+ }
+ }
+ return modified;
+ }
+
+ /** Merges the given manifest fragment into the given manifest file */
+ private static boolean mergeManifest(Document currentManifest, Document fragment) {
+ // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create
+ // and maintain error markers.
+
+ // Transfer package element from manifest to merged in root; required by
+ // manifest merger
+ Element fragmentRoot = fragment.getDocumentElement();
+ Element manifestRoot = currentManifest.getDocumentElement();
+ if (fragmentRoot == null || manifestRoot == null) {
+ return false;
+ }
+ String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg == null || pkg.isEmpty()) {
+ pkg = manifestRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg != null && !pkg.isEmpty()) {
+ fragmentRoot.setAttribute(ATTR_PACKAGE, pkg);
+ }
+ }
+
+ ManifestMerger merger = new ManifestMerger(
+ MergerLog.wrapSdkLog(AdtPlugin.getDefault()),
+ new AdtManifestMergeCallback()).setExtractPackagePrefix(true);
+ return currentManifest != null &&
+ fragment != null &&
+ merger.process(currentManifest, fragment);
+ }
+
+ /**
+ * Makes a backup of the given file, if it exists, by renaming it to name~
+ * (and removing an old name~ file if it exists)
+ */
+ private static boolean makeBackup(File file) {
+ if (!file.exists()) {
+ return true;
+ }
+ if (file.isDirectory()) {
+ return false;
+ }
+
+ File backupFile = new File(file.getParentFile(), file.getName() + '~');
+ if (backupFile.exists()) {
+ backupFile.delete();
+ }
+ return file.renameTo(backupFile);
+ }
+
+ private static String getResourceId(Element element) {
+ String name = element.getAttribute(ATTR_NAME);
+ if (name == null) {
+ name = element.getAttribute(ATTR_ID);
+ }
+
+ return name;
+ }
+
+ /** Instantiates the given template file into the given output file */
+ private void instantiate(
+ @NonNull final Configuration freemarker,
+ @NonNull final Map<String, Object> paramMap,
+ @NonNull String relativeFrom,
+ @NonNull IPath to) throws IOException, TemplateException {
+ // For now, treat extension-less files as directories... this isn't quite right
+ // so I should refine this! Maybe with a unique attribute in the template file?
+ boolean isDirectory = relativeFrom.indexOf('.') == -1;
+ if (isDirectory) {
+ // It's a directory
+ copyTemplateResource(relativeFrom, to);
+ } else {
+ File from = getFullPath(relativeFrom);
+ mLoader.setTemplateFile(from);
+ Template template = freemarker.getTemplate(from.getName());
+ Writer out = new StringWriter(1024);
+ template.process(paramMap, out);
+ out.flush();
+ String contents = out.toString();
+
+ contents = format(mProject, contents, to);
+ IFile targetFile = getTargetFile(to);
+ TextFileChange change = createNewFileChange(targetFile);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new InsertEdit(0, contents));
+ change.setEdit(rootEdit);
+ mTextChanges.add(change);
+ }
+ }
+
+ private static String format(IProject project, String contents, IPath to) {
+ String name = to.lastSegment();
+ if (name.endsWith(DOT_XML)) {
+ XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to);
+ EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
+ return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null);
+ } else if (name.endsWith(DOT_JAVA)) {
+ Map<?, ?> options = null;
+ if (project != null && project.isAccessible()) {
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ options = javaProject.getOptions(true);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ if (options == null) {
+ options = JavaCore.getOptions();
+ }
+
+ CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
+
+ try {
+ IDocument doc = new org.eclipse.jface.text.Document();
+ // format the file (the meat and potatoes)
+ doc.set(contents);
+ TextEdit edit = formatter.format(
+ CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS,
+ contents, 0, contents.length(), 0, null);
+ if (edit != null) {
+ edit.apply(doc);
+ }
+
+ return doc.get();
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return contents;
+ }
+
+ private static TextFileChange createNewFileChange(IFile targetFile) {
+ String fileName = targetFile.getName();
+ String message;
+ if (targetFile.exists()) {
+ message = String.format("Replace %1$s", fileName);
+ } else {
+ message = String.format("Create %1$s", fileName);
+ }
+
+ TextFileChange change = new TextFileChange(message, targetFile) {
+ @Override
+ protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException {
+ IDocument document = super.acquireDocument(pm);
+
+ // In our case, we know we *always* use this TextFileChange
+ // to *create* files, we're not appending to existing files.
+ // However, due to the following bug we can end up with cached
+ // contents of previously deleted files that happened to have the
+ // same file name:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402
+ // Therefore, as a workaround, wipe out the cached contents here
+ if (document.getLength() > 0) {
+ try {
+ document.replace(0, document.getLength(), "");
+ } catch (BadLocationException e) {
+ // pass
+ }
+ }
+
+ return document;
+ }
+ };
+ change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1));
+ return change;
+ }
+
+ /**
+ * Returns the list of files to open when the template has been created
+ *
+ * @return the list of files to open
+ */
+ @NonNull
+ public List<String> getFilesToOpen() {
+ return mOpen;
+ }
+
+ /**
+ * Returns the list of actions to perform when the template has been created
+ *
+ * @return the list of actions to perform
+ */
+ @NonNull
+ public List<Runnable> getFinalizingActions() {
+ return mFinalizingActions;
+ }
+
+ /** Copy a template resource */
+ private final void copyTemplateResource(
+ @NonNull String relativeFrom,
+ @NonNull IPath output) throws IOException {
+ File from = getFullPath(relativeFrom);
+ copy(from, output);
+ }
+
+ /** Returns true if the given file contains the given bytes */
+ private static boolean isIdentical(@Nullable byte[] data, @NonNull IFile dest) {
+ assert dest.exists();
+ byte[] existing = AdtUtils.readData(dest);
+ return Arrays.equals(existing, data);
+ }
+
+ /**
+ * Copies the given source file into the given destination file (where the
+ * source is allowed to be a directory, in which case the whole directory is
+ * copied recursively)
+ */
+ private void copy(File src, IPath path) throws IOException {
+ if (src.isDirectory()) {
+ File[] children = src.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ copy(child, path.append(child.getName()));
+ }
+ }
+ } else {
+ IResource dest = mProject.getFile(path);
+ if (dest.exists() && !(dest instanceof IFile)) {// Don't attempt to overwrite a folder
+ assert false : dest.getClass().getName();
+ return;
+ }
+ IFile file = (IFile) dest;
+ String targetName = path.lastSegment();
+ if (dest instanceof IFile) {
+ if (dest.exists() && isIdentical(Files.toByteArray(src), file)) {
+ String label = String.format(
+ "Not overwriting %1$s because the files are identical", targetName);
+ NullChange change = new NullChange(label);
+ change.setEnabled(false);
+ mOtherChanges.add(change);
+ return;
+ }
+ }
+
+ if (targetName.endsWith(DOT_XML)
+ || targetName.endsWith(DOT_JAVA)
+ || targetName.endsWith(DOT_TXT)
+ || targetName.endsWith(DOT_RS)
+ || targetName.endsWith(DOT_AIDL)
+ || targetName.endsWith(DOT_SVG)) {
+
+ String newFile = Files.toString(src, Charsets.UTF_8);
+ newFile = format(mProject, newFile, path);
+
+ TextFileChange addFile = createNewFileChange(file);
+ addFile.setEdit(new InsertEdit(0, newFile));
+ mTextChanges.add(addFile);
+ } else {
+ // Write binary file: Need custom change for that
+ IPath workspacePath = mProject.getFullPath().append(path);
+ mOtherChanges.add(new CreateFileChange(targetName, workspacePath, src));
+ }
+ }
+ }
+
+ /**
+ * A custom {@link TemplateLoader} which locates and provides templates
+ * within the plugin .jar file
+ */
+ private static final class MyTemplateLoader implements TemplateLoader {
+ private String mPrefix;
+
+ public void setPrefix(String prefix) {
+ mPrefix = prefix;
+ }
+
+ public void setTemplateFile(File file) {
+ setTemplateParent(file.getParentFile());
+ }
+
+ public void setTemplateParent(File parent) {
+ mPrefix = parent.getPath();
+ }
+
+ @Override
+ public Reader getReader(Object templateSource, String encoding) throws IOException {
+ URL url = (URL) templateSource;
+ return new InputStreamReader(url.openStream(), encoding);
+ }
+
+ @Override
+ public long getLastModified(Object templateSource) {
+ return 0;
+ }
+
+ @Override
+ public Object findTemplateSource(String name) throws IOException {
+ String path = mPrefix != null ? mPrefix + '/' + name : name;
+ File file = new File(path);
+ if (file.exists()) {
+ return file.toURI().toURL();
+ }
+ return null;
+ }
+
+ @Override
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+ }
+
+ /**
+ * Validates this template to make sure it's supported
+ * @param currentMinSdk the minimum SDK in the project, or -1 or 0 if unknown (e.g. codename)
+ * @param buildApi the build API, or -1 or 0 if unknown (e.g. codename)
+ *
+ * @return a status object with the error, or null if there is no problem
+ */
+ @SuppressWarnings("cast") // In Eclipse 3.6.2 cast below is needed
+ @Nullable
+ public IStatus validateTemplate(int currentMinSdk, int buildApi) {
+ TemplateMetadata template = getTemplate();
+ if (template == null) {
+ return null;
+ }
+ if (!template.isSupported()) {
+ String versionString = (String) AdtPlugin.getDefault().getBundle().getHeaders().get(
+ Constants.BUNDLE_VERSION);
+ Version version = new Version(versionString);
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a more recent version of the " +
+ "Android Eclipse plugin. Please update from version %1$d.%2$d.%3$d.",
+ version.getMajor(), version.getMinor(), version.getMicro()));
+ }
+ int templateMinSdk = template.getMinSdk();
+ if (templateMinSdk > currentMinSdk && currentMinSdk >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a minimum SDK version of at " +
+ "least %1$d, and the current min version is %2$d",
+ templateMinSdk, currentMinSdk));
+ }
+ int templateMinBuildApi = template.getMinBuildApi();
+ if (templateMinBuildApi > buildApi && buildApi >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a build target API version of at " +
+ "least %1$d, and the current version is %2$d",
+ templateMinBuildApi, buildApi));
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java
new file mode 100644
index 000000000..30dd09e31
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.SdkConstants.FD_EXTRAS;
+import static com.android.SdkConstants.FD_TEMPLATES;
+import static com.android.SdkConstants.FD_TOOLS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TEMPLATE_XML;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+
+import org.w3c.dom.Document;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Handles locating templates and providing template metadata */
+public class TemplateManager {
+ private static final Set<String> EXCLUDED_CATEGORIES = Sets.newHashSet("Folder", "Google");
+ private static final Set<String> EXCLUDED_FORMFACTORS = Sets.newHashSet("Wear", "TV");
+
+ TemplateManager() {
+ }
+
+ /** @return the root folder containing templates */
+ @Nullable
+ public static File getTemplateRootFolder() {
+ String location = AdtPrefs.getPrefs().getOsSdkFolder();
+ if (location != null) {
+ File folder = new File(location, FD_TOOLS + File.separator + FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ return folder;
+ }
+ }
+
+ return null;
+ }
+
+ /** @return the root folder containing extra templates */
+ @NonNull
+ public static List<File> getExtraTemplateRootFolders() {
+ List<File> folders = new ArrayList<File>();
+ String location = AdtPrefs.getPrefs().getOsSdkFolder();
+ if (location != null) {
+ File extras = new File(location, FD_EXTRAS);
+ if (extras.isDirectory()) {
+ for (File vendor : AdtUtils.listFiles(extras)) {
+ if (!vendor.isDirectory()) {
+ continue;
+ }
+ for (File pkg : AdtUtils.listFiles(vendor)) {
+ if (pkg.isDirectory()) {
+ File folder = new File(pkg, FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ folders.add(folder);
+ }
+ }
+ }
+ }
+
+ // Legacy
+ File folder = new File(extras, FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ folders.add(folder);
+ }
+ }
+ }
+
+ return folders;
+ }
+
+ /**
+ * Returns a template file under the given root, if it exists
+ *
+ * @param root the root folder
+ * @param relativePath the relative path
+ * @return a template file under the given root, if it exists
+ */
+ @Nullable
+ public static File getTemplateLocation(@NonNull File root, @NonNull String relativePath) {
+ File templateRoot = getTemplateRootFolder();
+ if (templateRoot != null) {
+ String rootPath = root.getPath();
+ File templateFile = new File(templateRoot,
+ rootPath.replace('/', File.separatorChar) + File.separator
+ + relativePath.replace('/', File.separatorChar));
+ if (templateFile.exists()) {
+ return templateFile;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a template file under one of the available roots, if it exists
+ *
+ * @param relativePath the relative path
+ * @return a template file under one of the available roots, if it exists
+ */
+ @Nullable
+ public static File getTemplateLocation(@NonNull String relativePath) {
+ File templateRoot = getTemplateRootFolder();
+ if (templateRoot != null) {
+ File templateFile = new File(templateRoot,
+ relativePath.replace('/', File.separatorChar));
+ if (templateFile.exists()) {
+ return templateFile;
+ }
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Returns all the templates with the given prefix
+ *
+ * @param folder the folder prefix
+ * @return the available templates
+ */
+ @NonNull
+ List<File> getTemplates(@NonNull String folder) {
+ List<File> templates = new ArrayList<File>();
+ Map<String, File> templateNames = Maps.newHashMap();
+ File root = getTemplateRootFolder();
+ if (root != null) {
+ File[] files = new File(root, folder).listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) { // Avoid .DS_Store etc
+ templates.add(file);
+ templateNames.put(file.getName(), file);
+ }
+ }
+ }
+ }
+
+ // Add in templates from extras/ as well.
+ for (File extra : getExtraTemplateRootFolders()) {
+ File[] files = new File(extra, folder).listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ File replaces = templateNames.get(file.getName());
+ if (replaces != null) {
+ int compare = compareTemplates(replaces, file);
+ if (compare > 0) {
+ int index = templates.indexOf(replaces);
+ if (index != -1) {
+ templates.set(index, file);
+ } else {
+ templates.add(file);
+ }
+ }
+ } else {
+ templates.add(file);
+ }
+ }
+ }
+ }
+ }
+
+ // Sort by file name (not path as is File's default)
+ if (templates.size() > 1) {
+ Collections.sort(templates, new Comparator<File>() {
+ @Override
+ public int compare(File file1, File file2) {
+ return file1.getName().compareTo(file2.getName());
+ }
+ });
+ }
+
+ return templates;
+ }
+
+ /**
+ * Compare two files, and return the one with the HIGHEST revision, and if
+ * the same, most recently modified
+ */
+ private int compareTemplates(File file1, File file2) {
+ TemplateMetadata template1 = getTemplate(file1);
+ TemplateMetadata template2 = getTemplate(file2);
+
+ if (template1 == null) {
+ return 1;
+ } else if (template2 == null) {
+ return -1;
+ } else {
+ int delta = template2.getRevision() - template1.getRevision();
+ if (delta == 0) {
+ delta = (int) (file2.lastModified() - file1.lastModified());
+ }
+ return delta;
+ }
+ }
+
+ /** Cache for {@link #getTemplate()} */
+ private Map<File, TemplateMetadata> mTemplateMap;
+
+ @Nullable
+ TemplateMetadata getTemplate(File templateDir) {
+ if (mTemplateMap != null) {
+ TemplateMetadata metadata = mTemplateMap.get(templateDir);
+ if (metadata != null) {
+ return metadata;
+ }
+ } else {
+ mTemplateMap = Maps.newHashMap();
+ }
+
+ try {
+ File templateFile = new File(templateDir, TEMPLATE_XML);
+ if (templateFile.isFile()) {
+ String xml = Files.toString(templateFile, Charsets.UTF_8);
+ Document doc = DomUtilities.parseDocument(xml, true);
+ if (doc != null && doc.getDocumentElement() != null) {
+ TemplateMetadata metadata = new TemplateMetadata(doc);
+ if (EXCLUDED_CATEGORIES.contains(metadata.getCategory()) ||
+ EXCLUDED_FORMFACTORS.contains(metadata.getFormFactor())) {
+ return null;
+ }
+ mTemplateMap.put(templateDir, metadata);
+ return metadata;
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java
new file mode 100644
index 000000000..4ce7d74c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_REVISION;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_BACKGROUND;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CLIPART_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DESCRIPTION;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_FOREGROUND;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_FORMAT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_PADDING;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SHAPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SOURCE_TYPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TEXT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TRIM;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TYPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_VALUE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.CURRENT_FORMAT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_DEPENDENCY;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_ICONS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_PARAMETER;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_THUMB;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_FORMFACTOR;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_CATEGORY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.assetstudiolib.GraphicGenerator;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.assetstudio.AssetType;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState.SourceType;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
+import com.android.utils.Pair;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.RGB;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/** An ADT template along with metadata */
+class TemplateMetadata {
+ private final Document mDocument;
+ private final List<Parameter> mParameters;
+ private final Map<String, Parameter> mParameterMap;
+ private List<Pair<String, Integer>> mDependencies;
+ private Integer mMinApi;
+ private Integer mMinBuildApi;
+ private Integer mRevision;
+ private boolean mNoIcons;
+ private CreateAssetSetWizardState mIconState;
+ private String mFormFactor;
+ private String mCategory;
+
+ TemplateMetadata(@NonNull Document document) {
+ mDocument = document;
+
+ NodeList parameters = mDocument.getElementsByTagName(TAG_PARAMETER);
+ mParameters = new ArrayList<Parameter>(parameters.getLength());
+ mParameterMap = new HashMap<String, Parameter>(parameters.getLength());
+ for (int index = 0, max = parameters.getLength(); index < max; index++) {
+ Element element = (Element) parameters.item(index);
+ Parameter parameter = new Parameter(this, element);
+ mParameters.add(parameter);
+ if (parameter.id != null) {
+ mParameterMap.put(parameter.id, parameter);
+ }
+ }
+ }
+
+ boolean isSupported() {
+ String versionString = mDocument.getDocumentElement().getAttribute(ATTR_FORMAT);
+ if (versionString != null && !versionString.isEmpty()) {
+ try {
+ int version = Integer.parseInt(versionString);
+ return version <= CURRENT_FORMAT;
+ } catch (NumberFormatException nufe) {
+ return false;
+ }
+ }
+
+ // Older templates without version specified: supported
+ return true;
+ }
+
+ @Nullable
+ String getTitle() {
+ String name = mDocument.getDocumentElement().getAttribute(ATTR_NAME);
+ if (name != null && !name.isEmpty()) {
+ return name;
+ }
+
+ return null;
+ }
+
+ @Nullable
+ String getDescription() {
+ String description = mDocument.getDocumentElement().getAttribute(ATTR_DESCRIPTION);
+ if (description != null && !description.isEmpty()) {
+ return description;
+ }
+
+ return null;
+ }
+
+ int getMinSdk() {
+ if (mMinApi == null) {
+ mMinApi = 1;
+ String api = mDocument.getDocumentElement().getAttribute(ATTR_MIN_API);
+ if (api != null && !api.isEmpty()) {
+ try {
+ mMinApi = Integer.parseInt(api);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should always be an integer
+ AdtPlugin.log(nufe, null);
+ mMinApi = 1;
+ }
+ }
+ }
+
+ return mMinApi.intValue();
+ }
+
+ int getMinBuildApi() {
+ if (mMinBuildApi == null) {
+ mMinBuildApi = 1;
+ String api = mDocument.getDocumentElement().getAttribute(ATTR_MIN_BUILD_API);
+ if (api != null && !api.isEmpty()) {
+ try {
+ mMinBuildApi = Integer.parseInt(api);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should always be an integer
+ AdtPlugin.log(nufe, null);
+ mMinBuildApi = 1;
+ }
+ }
+ }
+
+ return mMinBuildApi.intValue();
+ }
+
+ public int getRevision() {
+ if (mRevision == null) {
+ mRevision = 1;
+ String revision = mDocument.getDocumentElement().getAttribute(ATTR_REVISION);
+ if (revision != null && !revision.isEmpty()) {
+ try {
+ mRevision = Integer.parseInt(revision);
+ } catch (NumberFormatException nufe) {
+ AdtPlugin.log(nufe, null);
+ mRevision = 1;
+ }
+ }
+ }
+
+ return mRevision.intValue();
+ }
+
+ public String getFormFactor() {
+ if (mFormFactor == null) {
+ mFormFactor = "Mobile";
+
+ NodeList formfactorDeclarations = mDocument.getElementsByTagName(TAG_FORMFACTOR);
+ if (formfactorDeclarations.getLength() > 0) {
+ Element element = (Element) formfactorDeclarations.item(0);
+ String formFactor = element.getAttribute(ATTR_VALUE);
+ if (formFactor != null && !formFactor.isEmpty()) {
+ mFormFactor = formFactor;
+ }
+ }
+ }
+ return mFormFactor;
+ }
+
+ public String getCategory() {
+ if (mCategory == null) {
+ mCategory = "";
+ NodeList categories = mDocument.getElementsByTagName(TAG_CATEGORY);
+ if (categories.getLength() > 0) {
+ Element element = (Element) categories.item(0);
+ String category = element.getAttribute(ATTR_VALUE);
+ if (category != null && !category.isEmpty()) {
+ mCategory = category;
+ }
+ }
+ }
+ return mCategory;
+ }
+
+ /**
+ * Returns a suitable icon wizard state instance if this wizard requests
+ * icons to be created, and null otherwise
+ *
+ * @return icon wizard state or null
+ */
+ @Nullable
+ public CreateAssetSetWizardState getIconState(IProject project) {
+ if (mIconState == null && !mNoIcons) {
+ NodeList icons = mDocument.getElementsByTagName(TAG_ICONS);
+ if (icons.getLength() < 1) {
+ mNoIcons = true;
+ return null;
+ }
+ Element icon = (Element) icons.item(0);
+
+ mIconState = new CreateAssetSetWizardState();
+ mIconState.project = project;
+
+ String typeString = getAttributeOrNull(icon, ATTR_TYPE);
+ if (typeString != null) {
+ typeString = typeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (AssetType type : AssetType.values()) {
+ if (typeString.equals(type.name())) {
+ mIconState.type = type;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown asset type %1$s", typeString);
+ }
+ }
+
+ mIconState.outputName = getAttributeOrNull(icon, ATTR_NAME);
+ if (mIconState.outputName != null) {
+ // Register parameter such that if it is referencing other values, it gets
+ // updated when other values are edited
+ Parameter outputParameter = new Parameter(this,
+ Parameter.Type.STRING, "_iconname", mIconState.outputName); //$NON-NLS-1$
+ getParameters().add(outputParameter);
+ }
+
+ RGB background = getRgb(icon, ATTR_BACKGROUND);
+ if (background != null) {
+ mIconState.background = background;
+ }
+ RGB foreground = getRgb(icon, ATTR_FOREGROUND);
+ if (foreground != null) {
+ mIconState.foreground = foreground;
+ }
+ String shapeString = getAttributeOrNull(icon, ATTR_SHAPE);
+ if (shapeString != null) {
+ shapeString = shapeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (GraphicGenerator.Shape shape : GraphicGenerator.Shape.values()) {
+ if (shapeString.equals(shape.name())) {
+ mIconState.shape = shape;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown shape %1$s", shapeString);
+ }
+ }
+ String trimString = getAttributeOrNull(icon, ATTR_TRIM);
+ if (trimString != null) {
+ mIconState.trim = Boolean.valueOf(trimString);
+ }
+ String paddingString = getAttributeOrNull(icon, ATTR_PADDING);
+ if (paddingString != null) {
+ mIconState.padding = Integer.parseInt(paddingString);
+ }
+ String sourceTypeString = getAttributeOrNull(icon, ATTR_SOURCE_TYPE);
+ if (sourceTypeString != null) {
+ sourceTypeString = sourceTypeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (SourceType type : SourceType.values()) {
+ if (sourceTypeString.equals(type.name())) {
+ mIconState.sourceType = type;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown source type %1$s", sourceTypeString);
+ }
+ }
+ mIconState.clipartName = getAttributeOrNull(icon, ATTR_CLIPART_NAME);
+
+ String textString = getAttributeOrNull(icon, ATTR_TEXT);
+ if (textString != null) {
+ mIconState.text = textString;
+ }
+ }
+
+ return mIconState;
+ }
+
+ void updateIconName(List<Parameter> parameters, StringEvaluator evaluator) {
+ if (mIconState != null) {
+ NodeList icons = mDocument.getElementsByTagName(TAG_ICONS);
+ if (icons.getLength() < 1) {
+ return;
+ }
+ Element icon = (Element) icons.item(0);
+ String name = getAttributeOrNull(icon, ATTR_NAME);
+ if (name != null) {
+ mIconState.outputName = evaluator.evaluate(name, parameters);
+ }
+ }
+ }
+
+ private static RGB getRgb(@NonNull Element element, @NonNull String name) {
+ String colorString = getAttributeOrNull(element, name);
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ return ImageUtils.intToRgb(rgb);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static String getAttributeOrNull(@NonNull Element element, @NonNull String name) {
+ String value = element.getAttribute(name);
+ if (value != null && value.isEmpty()) {
+ return null;
+ }
+ return value;
+ }
+
+ @Nullable
+ String getThumbnailPath() {
+ // Apply selector logic. Pick the thumb first thumb that satisfies the largest number
+ // of conditions.
+ NodeList thumbs = mDocument.getElementsByTagName(TAG_THUMB);
+ if (thumbs.getLength() == 0) {
+ return null;
+ }
+
+
+ int bestMatchCount = 0;
+ Element bestMatch = null;
+
+ for (int i = 0, n = thumbs.getLength(); i < n; i++) {
+ Element thumb = (Element) thumbs.item(i);
+
+ NamedNodeMap attributes = thumb.getAttributes();
+ if (bestMatch == null && attributes.getLength() == 0) {
+ bestMatch = thumb;
+ } else if (attributes.getLength() <= bestMatchCount) {
+ // Already have a match with this number of attributes, no point checking
+ continue;
+ } else {
+ boolean match = true;
+ for (int j = 0, max = attributes.getLength(); j < max; j++) {
+ Attr attribute = (Attr) attributes.item(j);
+ Parameter parameter = mParameterMap.get(attribute.getName());
+ if (parameter == null) {
+ AdtPlugin.log(null, "Unexpected parameter in template thumbnail: %1$s",
+ attribute.getName());
+ continue;
+ }
+ String thumbNailValue = attribute.getValue();
+ String editedValue = parameter.value != null ? parameter.value.toString() : "";
+ if (!thumbNailValue.equals(editedValue)) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ bestMatch = thumb;
+ bestMatchCount = attributes.getLength();
+ }
+ }
+ }
+
+ if (bestMatch != null) {
+ NodeList children = bestMatch.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.TEXT_NODE) {
+ return child.getNodeValue().trim();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the dependencies (as a list of pairs of names and revisions)
+ * required by this template
+ */
+ List<Pair<String, Integer>> getDependencies() {
+ if (mDependencies == null) {
+ NodeList elements = mDocument.getElementsByTagName(TAG_DEPENDENCY);
+ if (elements.getLength() == 0) {
+ return Collections.emptyList();
+ }
+
+ List<Pair<String, Integer>> dependencies = Lists.newArrayList();
+ for (int i = 0, n = elements.getLength(); i < n; i++) {
+ Element element = (Element) elements.item(i);
+ String name = element.getAttribute(ATTR_NAME);
+ int revision = -1;
+ String revisionString = element.getAttribute(ATTR_REVISION);
+ if (!revisionString.isEmpty()) {
+ revision = Integer.parseInt(revisionString);
+ }
+ dependencies.add(Pair.of(name, revision));
+ }
+ mDependencies = dependencies;
+ }
+
+ return mDependencies;
+ }
+
+ /** Returns the list of available parameters */
+ @NonNull
+ List<Parameter> getParameters() {
+ return mParameters;
+ }
+
+ /**
+ * Returns the parameter of the given id, or null if not found
+ *
+ * @param id the id of the target parameter
+ * @return the corresponding parameter, or null if not found
+ */
+ @Nullable
+ public Parameter getParameter(@NonNull String id) {
+ for (Parameter parameter : mParameters) {
+ if (id.equals(parameter.id)) {
+ return parameter;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns a default icon for templates */
+ static Image getDefaultTemplateIcon() {
+ return IconFactory.getInstance().getIcon("default_template"); //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java
new file mode 100644
index 000000000..c3d28fcf2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.internal.ui.refactoring.PreviewWizardPage;
+
+import java.util.List;
+
+@SuppressWarnings("restriction") // Refactoring UI
+class TemplatePreviewPage extends PreviewWizardPage {
+ private final NewTemplateWizardState mValues;
+
+ TemplatePreviewPage(NewTemplateWizardState values) {
+ super(true);
+ mValues = values;
+ setTitle("Preview");
+ setDescription("Optionally review pending changes");
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ List<Change> changes = mValues.computeChanges();
+ CompositeChange root = new CompositeChange("Create template",
+ changes.toArray(new Change[changes.size()]));
+ setChange(root);
+ }
+
+ super.setVisible(visible);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java
new file mode 100644
index 000000000..e461d5597
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/** For template developers: Test local template directory */
+public class TemplateTestPage extends WizardPage
+ implements SelectionListener, ModifyListener {
+ private Text mLocation;
+ private Button mButton;
+ private static String sLocation; // Persist between repeated invocations
+ private Button mProjectToggle;
+ private File mTemplate;
+
+ TemplateTestPage() {
+ super("testWizardPage"); //$NON-NLS-1$
+ setTitle("Wizard Tester");
+ setDescription("Test a new template");
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(3, false));
+
+ Label label = new Label(container, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ label.setText("Template Location:");
+
+ mLocation = new Text(container, SWT.BORDER);
+ GridData gd_mLocation = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1);
+ gd_mLocation.widthHint = 400;
+ mLocation.setLayoutData(gd_mLocation);
+ if (sLocation != null) {
+ mLocation.setText(sLocation);
+ }
+ mLocation.addModifyListener(this);
+
+ mButton = new Button(container, SWT.FLAT);
+ mButton.setText("...");
+
+ mProjectToggle = new Button(container, SWT.CHECK);
+ mProjectToggle.setEnabled(false);
+ mProjectToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ mProjectToggle.setText("Full project template");
+ new Label(container, SWT.NONE);
+ mButton.addSelectionListener(this);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ validatePage();
+ }
+
+ private boolean validatePage() {
+ String error = null;
+
+ String path = mLocation.getText().trim();
+ if (path == null || path.length() == 0) {
+ error = "Select a template directory";
+ mTemplate = null;
+ } else {
+ mTemplate = new File(path);
+ if (!mTemplate.exists()) {
+ error = String.format("%1$s does not exist", path);
+ } else {
+ // Preserve across wizard sessions
+ sLocation = path;
+
+ if (mTemplate.isDirectory()) {
+ if (!new File(mTemplate, TemplateHandler.TEMPLATE_XML).exists()) {
+ error = String.format("Not a template: missing template.xml file in %1$s ",
+ path);
+ }
+ } else {
+ if (mTemplate.getName().equals(TemplateHandler.TEMPLATE_XML)) {
+ mTemplate = mTemplate.getParentFile();
+ } else {
+ error = String.format("Select a directory containing a template");
+ }
+ }
+ }
+ }
+
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, IMessageProvider.ERROR);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+
+ return error == null;
+ }
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mButton) {
+ DirectoryDialog dialog = new DirectoryDialog(mButton.getShell(), SWT.OPEN);
+ String path = mLocation.getText().trim();
+ if (path.length() > 0) {
+ dialog.setFilterPath(path);
+ }
+ String file = dialog.open();
+ if (file != null) {
+ mLocation.setText(file);
+ }
+ }
+
+ validatePage();
+ }
+
+ File getLocation() {
+ return mTemplate;
+ }
+
+ boolean isProjectTemplate() {
+ return mProjectToggle.getSelection();
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java
new file mode 100644
index 000000000..b3b1ef2f4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+
+/**
+ * Template wizard which creates parameterized templates
+ */
+public class TemplateTestWizard extends NewTemplateWizard {
+ private TemplateTestPage mSelectionPage;
+ private IProject mProject;
+
+ /** Creates a new wizard for testing template definitions in a local directory */
+ public TemplateTestWizard() {
+ super("");
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+ if (mValues != null) {
+ mProject = mValues.project;
+ }
+
+ mMainPage = null;
+ mValues = null;
+
+ mSelectionPage = new TemplateTestPage();
+ }
+
+ @Override
+ public void addPages() {
+ addPage(mSelectionPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mSelectionPage) {
+ File file = mSelectionPage.getLocation();
+ if (file != null && file.exists()) {
+ if (mValues == null) {
+ mValues = new NewTemplateWizardState();
+ mValues.setTemplateLocation(file);
+ mValues.project = mProject;
+ hideBuiltinParameters();
+
+ mMainPage = new NewTemplatePage(mValues, true);
+ addPage(mMainPage);
+ } else {
+ mValues.setTemplateLocation(file);
+ }
+
+ return mMainPage;
+ }
+ }
+
+ return super.getNextPage(page);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java
new file mode 100644
index 000000000..7ca32f91f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static org.eclipse.core.resources.IResource.DEPTH_INFINITE;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.actions.WorkspaceModifyOperation;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.swing.SwingUtilities;
+
+abstract class TemplateWizard extends Wizard implements INewWizard {
+ private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$
+ protected IWorkbench mWorkbench;
+ private UpdateToolsPage mUpdatePage;
+ private InstallDependencyPage mDependencyPage;
+ private TemplatePreviewPage mPreviewPage;
+ protected ConfigureAssetSetPage mIconPage;
+
+ protected TemplateWizard() {
+ }
+
+ /** Should this wizard add an icon page? */
+ protected boolean shouldAddIconPage() {
+ return false;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ mWorkbench = workbench;
+
+ setHelpAvailable(false);
+ ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+
+ if (!UpdateToolsPage.isUpToDate()) {
+ mUpdatePage = new UpdateToolsPage();
+ }
+
+ setNeedsProgressMonitor(true);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ if (mUpdatePage != null) {
+ addPage(mUpdatePage);
+ }
+ }
+
+ @Override
+ public IWizardPage getStartingPage() {
+ if (mUpdatePage != null && mUpdatePage.isPageComplete()) {
+ return getNextPage(mUpdatePage);
+ }
+
+ return super.getStartingPage();
+ }
+
+ protected WizardPage getPreviewPage(NewTemplateWizardState values) {
+ if (mPreviewPage == null) {
+ mPreviewPage = new TemplatePreviewPage(values);
+ addPage(mPreviewPage);
+ }
+
+ return mPreviewPage;
+ }
+
+ protected WizardPage getIconPage(CreateAssetSetWizardState iconState) {
+ if (mIconPage == null) {
+ mIconPage = new ConfigureAssetSetPage(iconState);
+ mIconPage.setTitle("Configure Icon");
+ addPage(mIconPage);
+ }
+
+ return mIconPage;
+ }
+
+ protected WizardPage getDependencyPage(TemplateMetadata template, boolean create) {
+ if (!create) {
+ return mDependencyPage;
+ }
+
+ if (mDependencyPage == null) {
+ mDependencyPage = new InstallDependencyPage();
+ addPage(mDependencyPage);
+ }
+ mDependencyPage.setTemplate(template);
+ return mDependencyPage;
+ }
+
+ /**
+ * Returns the project where the template is being inserted
+ *
+ * @return the project to insert the template into
+ */
+ @NonNull
+ protected abstract IProject getProject();
+
+ /**
+ * Returns the list of files to open, which might be empty. This method will
+ * only be called <b>after</b> {@link #computeChanges()} has been called.
+ *
+ * @return a list of files to open
+ */
+ @NonNull
+ protected abstract List<String> getFilesToOpen();
+
+ /**
+ * Returns the list of files to open, which might be empty. This method will
+ * only be called <b>after</b> {@link #computeChanges()} has been called.
+ *
+ * @return a list of files to open
+ */
+ @NonNull
+ protected abstract List<Runnable> getFinalizingActions();
+
+ /**
+ * Computes the changes to the {@link #getProject()} this template should
+ * perform
+ *
+ * @return the changes to perform
+ */
+ protected abstract List<Change> computeChanges();
+
+ protected boolean performFinish(IProgressMonitor monitor) throws InvocationTargetException {
+ List<Change> changes = computeChanges();
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating template...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ // TBD: Is this necessary now that we're using IFile objects?
+ try {
+ getProject().refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean performFinish() {
+ final AtomicBoolean success = new AtomicBoolean();
+ try {
+ getContainer().run(true, false, new IRunnableWithProgress() {
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException,
+ InterruptedException {
+ boolean ok = performFinish(monitor);
+ success.set(ok);
+ }
+ });
+
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ } catch (InterruptedException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ }
+
+ if (success.get()) {
+ // Open the primary file/files
+ NewTemplateWizard.openFiles(getProject(), getFilesToOpen(), mWorkbench);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java
new file mode 100644
index 000000000..468a10c77
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java
@@ -0,0 +1,50 @@
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import java.util.Locale;
+
+import org.xml.sax.Attributes;
+
+public class TypedVariable {
+ public enum Type {
+ STRING,
+ BOOLEAN,
+ INTEGER;
+
+ public static Type get(String name) {
+ if (name == null) {
+ return STRING;
+ }
+ try {
+ return valueOf(name.toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ System.err.println("Unexpected global type '" + name + "'");
+ System.err.println("Expected one of :");
+ for (Type s : Type.values()) {
+ System.err.println(" " + s.name().toLowerCase(Locale.US));
+ }
+ }
+
+ return STRING;
+ }
+ }
+
+ public static Object parseGlobal(Attributes attributes) {
+ String value = attributes.getValue(TemplateHandler.ATTR_VALUE);
+ Type type = Type.get(attributes.getValue(TemplateHandler.ATTR_TYPE));
+
+ switch (type) {
+ case STRING:
+ return value;
+ case BOOLEAN:
+ return Boolean.parseBoolean(value);
+ case INTEGER:
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return value;
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java
new file mode 100644
index 000000000..5bbf449d4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+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.Label;
+
+class UpdateToolsPage extends WizardPage implements SelectionListener {
+ private Button mInstallButton;
+ UpdateToolsPage() {
+ super("update");
+ setTitle("Update Tools");
+ validatePage();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(1, false));
+
+ Label label = new Label(container, SWT.WRAP);
+ GridData layoutData = new GridData(SWT.LEFT, SWT.TOP, true, true, 1, 1);
+ layoutData.widthHint = NewTemplatePage.WIZARD_PAGE_WIDTH - 50;
+ label.setLayoutData(layoutData);
+ label.setText(
+ "Your tools installation appears to be out of date (or not yet installed).\n" +
+ "\n" +
+ "This wizard depends on templates distributed with the Android SDK Tools.\n" +
+ "\n" +
+ "Please update the tools first (via Window > Android SDK Manager, or by " +
+ "using the \"android\" command in a terminal window). Note that on Windows " +
+ "you may need to restart the IDE, since there are some known problems where " +
+ "Windows locks the files held open by the running IDE, so the updater is " +
+ "unable to delete them in order to upgrade them.");
+
+ mInstallButton = new Button(container, SWT.NONE);
+ mInstallButton.setText("Check Again");
+ mInstallButton.addSelectionListener(this);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ return isUpToDate();
+ }
+
+ static boolean isUpToDate() {
+ return TemplateManager.getTemplateRootFolder() != null;
+ }
+
+ private void validatePage() {
+ boolean ok = isUpToDate();
+ setPageComplete(ok);
+ if (ok) {
+ setErrorMessage(null);
+ setMessage(null);
+ } else {
+ setErrorMessage("The tools need to be updated via the SDK Manager");
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mInstallButton) {
+ validatePage();
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}