From 99b9ad2143e5e9e626879309ecc6f883403bdcae Mon Sep 17 00:00:00 2001 From: Xavier Ducrohet Date: Wed, 13 May 2009 16:58:07 -0700 Subject: ADT: Move more packages into internal project.* refactorings.* Also: moved the export wizard from project.export to wizards.export, moved some actions out of project into the new internal package actions. --- .../META-INF/MANIFEST.MF | 3 +- .../plugins/com.android.ide.eclipse.adt/plugin.xml | 24 +- .../com/android/ide/eclipse/adt/AdtConstants.java | 2 +- .../src/com/android/ide/eclipse/adt/AdtPlugin.java | 6 +- .../internal/actions/ConvertToAndroidAction.java | 154 ++++ .../adt/internal/actions/FixProjectAction.java | 139 +++ .../ide/eclipse/adt/internal/build/ApkBuilder.java | 4 +- .../eclipse/adt/internal/build/BaseBuilder.java | 2 +- .../adt/internal/build/PreCompilerBuilder.java | 2 +- .../adt/internal/build/ResourceManagerBuilder.java | 2 +- .../internal/launch/AndroidLaunchController.java | 4 +- .../adt/internal/launch/LaunchConfigDelegate.java | 2 +- .../project/AndroidClasspathContainer.java | 60 ++ .../AndroidClasspathContainerInitializer.java | 645 ++++++++++++++ .../adt/internal/project/AndroidNature.java | 291 ++++++ .../adt/internal/project/ApkInstallManager.java | 207 +++++ .../adt/internal/project/FixLaunchConfig.java | 156 ++++ .../adt/internal/project/FolderDecorator.java | 107 +++ .../adt/internal/project/ProjectHelper.java | 770 ++++++++++++++++ .../internal/properties/AndroidPropertyPage.java | 123 +++ .../extractstring/ExtractStringAction.java | 170 ++++ .../extractstring/ExtractStringContribution.java | 53 ++ .../extractstring/ExtractStringDescriptor.java | 71 ++ .../extractstring/ExtractStringInputPage.java | 505 +++++++++++ .../extractstring/ExtractStringRefactoring.java | 988 +++++++++++++++++++++ .../extractstring/ExtractStringWizard.java | 50 ++ .../extractstring/XmlStringFileHelper.java | 122 +++ .../ide/eclipse/adt/project/AndroidNature.java | 291 ------ .../ide/eclipse/adt/project/ApkInstallManager.java | 207 ----- .../adt/project/ConvertToAndroidAction.java | 153 ---- .../ide/eclipse/adt/project/ExportAction.java | 65 -- .../eclipse/adt/project/ExportWizardAction.java | 57 -- .../ide/eclipse/adt/project/FixLaunchConfig.java | 156 ---- .../ide/eclipse/adt/project/FixProjectAction.java | 136 --- .../ide/eclipse/adt/project/FolderDecorator.java | 107 --- .../ide/eclipse/adt/project/ProjectHelper.java | 771 ---------------- .../eclipse/adt/project/export/ExportWizard.java | 570 ------------ .../eclipse/adt/project/export/KeyCheckPage.java | 443 --------- .../adt/project/export/KeyCreationPage.java | 332 ------- .../adt/project/export/KeySelectionPage.java | 266 ------ .../adt/project/export/KeystoreSelectionPage.java | 260 ------ .../adt/project/export/ProjectCheckPage.java | 302 ------- .../internal/AndroidClasspathContainer.java | 60 -- .../AndroidClasspathContainerInitializer.java | 646 -------------- .../project/properties/AndroidPropertyPage.java | 123 --- .../extractstring/ExtractStringAction.java | 170 ---- .../extractstring/ExtractStringContribution.java | 53 -- .../extractstring/ExtractStringDescriptor.java | 71 -- .../extractstring/ExtractStringInputPage.java | 505 ----------- .../extractstring/ExtractStringRefactoring.java | 988 --------------------- .../extractstring/ExtractStringWizard.java | 50 -- .../extractstring/XmlStringFileHelper.java | 122 --- .../src/com/android/ide/eclipse/adt/sdk/Sdk.java | 2 +- .../ide/eclipse/adt/ui/ReferenceChooserDialog.java | 4 +- .../ide/eclipse/adt/ui/ResourceChooser.java | 4 +- .../eclipse/adt/wizards/actions/ExportAction.java | 65 ++ .../adt/wizards/actions/ExportWizardAction.java | 57 ++ .../eclipse/adt/wizards/export/ExportWizard.java | 570 ++++++++++++ .../eclipse/adt/wizards/export/KeyCheckPage.java | 443 +++++++++ .../adt/wizards/export/KeyCreationPage.java | 332 +++++++ .../adt/wizards/export/KeySelectionPage.java | 266 ++++++ .../adt/wizards/export/KeystoreSelectionPage.java | 260 ++++++ .../adt/wizards/export/ProjectCheckPage.java | 302 +++++++ .../adt/wizards/newproject/NewProjectWizard.java | 4 +- .../sampleProjects/SampleProjectTest.java | 2 +- .../ide/eclipse/adt/build/BaseBuilderTest.java | 37 - .../adt/internal/build/BaseBuilderTest.java | 37 + .../adt/internal/project/ProjectHelperTest.java | 68 ++ .../ide/eclipse/adt/project/ProjectHelperTest.java | 67 -- 69 files changed, 7044 insertions(+), 7042 deletions(-) create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ApkInstallManager.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/XmlStringFileHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportWizardAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ExportWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCheckPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCreationPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeySelectionPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeystoreSelectionPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ProjectCheckPage.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/build/BaseBuilderTest.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/ProjectHelperTest.java delete mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index 51616f5..6dd1a5f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -49,8 +49,7 @@ Eclipse-LazyStart: true Export-Package: com.android.ide.eclipse.adt;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.build;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.launch;x-friends:="com.android.ide.eclipse.tests", - com.android.ide.eclipse.adt.project;x-friends:="com.android.ide.eclipse.tests", - com.android.ide.eclipse.adt.project.internal;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.project;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.sdk;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.wizards.newproject;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.ui;x-friends:="com.android.ide.eclipse.tests", diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index 8b7f4a2..faffa4b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -73,7 +73,7 @@ name="AndroidNature" point="org.eclipse.core.resources.natures"> - + @@ -209,7 +209,7 @@ @@ -351,7 +351,7 @@ @@ -388,7 +388,7 @@ point="org.eclipse.ui.decorators"> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java index 7304e5e..d3175ce 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java @@ -16,7 +16,7 @@ package com.android.ide.eclipse.adt; -import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index 697cf5b..8fc548c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -22,14 +22,14 @@ import com.android.ddmuilib.console.DdmConsole; import com.android.ddmuilib.console.IDdmConsole; import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController; import com.android.ide.eclipse.adt.internal.preferences.BuildPreferencePage; -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.export.ExportWizard; -import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.AndroidTargetParser; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.adt.ui.EclipseUiHelper; +import com.android.ide.eclipse.adt.wizards.export.ExportWizard; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.ExportHelper; import com.android.ide.eclipse.common.project.ExportHelper.IExportCallback; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java new file mode 100644 index 0000000..cfbc93d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/ConvertToAndroidAction.java @@ -0,0 +1,154 @@ +/* + * 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.actions; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +import java.util.Iterator; + +/** + * Converts a project created with the activity creator into an + * Android project. + */ +public class ConvertToAndroidAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /* + * (non-Javadoc) + * + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + // pass + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#run(IAction) + */ + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + for (Iterator it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject)element; + } else if (element instanceof IAdaptable) { + project = (IProject)((IAdaptable)element).getAdapter(IProject.class); + } + if (project != null) { + convertProject(project); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#selectionChanged(IAction, ISelection) + */ + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + /** + * Toggles sample nature on a project + * + * @param project to have sample nature added or removed + */ + private void convertProject(final IProject project) { + new Job("Convert Project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask(String.format( + "Convert %1$s to Android", project.getName()), 5); + } + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (AndroidConstants.NATURE.equals(natures[i])) { + // we shouldn't be here as the visibility of the item + // is dependent on the project. + return new Status(Status.WARNING, AdtPlugin.PLUGIN_ID, + "Project is already an Android project"); + } + } + + if (monitor != null) { + monitor.worked(1); + } + + String[] newNatures = new String[natures.length + 1]; + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = AndroidConstants.NATURE; + + // set the new nature list in the project + description.setNatureIds(newNatures); + project.setDescription(description, null); + if (monitor != null) { + monitor.worked(1); + } + + // Fix the classpath entries. + // get a java project + IJavaProject javaProject = JavaCore.create(project); + ProjectHelper.fixProjectClasspathEntries(javaProject); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java new file mode 100644 index 0000000..cb4f7e7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/FixProjectAction.java @@ -0,0 +1,139 @@ +/* + * 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.actions; + +import com.android.ide.eclipse.adt.internal.project.AndroidNature; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; + +import java.util.Iterator; + +/** + * Action to fix the project properties: + *
    + *
  • Make sure the framework archive is present with the link to the java + * doc
  • + *
+ */ +public class FixProjectAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + + for (Iterator it = ((IStructuredSelection) mSelection).iterator(); + it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element) + .getAdapter(IProject.class); + } + if (project != null) { + fixProject(project); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + private void fixProject(final IProject project) { + new Job("Fix Project Properties") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask("Fix Project Properties", 6); + } + + ProjectHelper.fixProject(project); + if (monitor != null) { + monitor.worked(1); + } + + // fix the nature order to have the proper project icon + ProjectHelper.fixProjectNatureOrder(project); + if (monitor != null) { + monitor.worked(1); + } + + // now we fix the builders + AndroidNature.configureResourceManagerBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configurePreBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configureApkBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } + + /** + * @see IWorkbenchWindowActionDelegate#init + */ + public void init(IWorkbenchWindow window) { + // pass + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java index cbe1fb3..29a1cb6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java @@ -19,8 +19,8 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.ApkInstallManager; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ApkInstallManager; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.sdk.DexWrapper; import com.android.ide.eclipse.adt.sdk.Sdk; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java index 03b0d2d..d773fa9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BaseBuilder.java @@ -19,7 +19,7 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.XmlErrorHandler; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java index 09097cf..6708d12 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/PreCompilerBuilder.java @@ -19,7 +19,7 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.FixLaunchConfig; +import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ResourceManagerBuilder.java index b5ef384..b490eac 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ResourceManagerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ResourceManagerBuilder.java @@ -19,7 +19,7 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.sdklib.IAndroidTarget; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java index 4fa2e7f..af40ae9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java @@ -32,8 +32,8 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode; import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode; import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse; -import com.android.ide.eclipse.adt.project.ApkInstallManager; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ApkInstallManager; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.adt.wizards.actions.AvdManagerAction; import com.android.ide.eclipse.common.project.AndroidManifestParser; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java index d3a2591..44e6fee 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java @@ -20,7 +20,7 @@ import com.android.ddmlib.AndroidDebugBridge; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.AndroidManifestParser.Activity; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java new file mode 100644 index 0000000..bbef6b9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainer.java @@ -0,0 +1,60 @@ +/* + * 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.project; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; + +/** + * Classpath container for the Android projects. + */ +class AndroidClasspathContainer implements IClasspathContainer { + + private IClasspathEntry[] mClasspathEntry; + private IPath mContainerPath; + private String mName; + + /** + * Constructs the container with the {@link IClasspathEntry} representing the android + * framework jar file and the container id + * @param entries the entries representing the android framework and optional libraries. + * @param path the path containing the classpath container id. + * @param name the name of the container to display. + */ + AndroidClasspathContainer(IClasspathEntry[] entries, IPath path, String name) { + mClasspathEntry = entries; + mContainerPath = path; + mName = name; + } + + public IClasspathEntry[] getClasspathEntries() { + return mClasspathEntry; + } + + public String getDescription() { + return mName; + } + + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + public IPath getPath() { + return mContainerPath; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java new file mode 100644 index 0000000..d0ab559 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java @@ -0,0 +1,645 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; + +import org.eclipse.core.resources.IMarker; +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.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.ClasspathContainerInitializer; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathContainer; +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 java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.regex.Pattern; + +/** + * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to + * {@link IProject}s. This removes the hard-coded path to the android.jar. + */ +public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { + /** The container id for the android framework jar file */ + private final static String CONTAINER_ID = + "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ + + /** path separator to store multiple paths in a single property. This is guaranteed to not + * be in a path. + */ + private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ + + private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ + private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ + private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ + private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; + + private final static int CACHE_INDEX_JAR = 0; + private final static int CACHE_INDEX_SRC = 1; + private final static int CACHE_INDEX_DOCS_URI = 2; + private final static int CACHE_INDEX_OPT_DOCS_URI = 3; + private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; + + public AndroidClasspathContainerInitializer() { + // pass + } + + /** + * Binds a classpath container to a {@link IClasspathContainer} for a given project, + * or silently fails if unable to do so. + * @param containerPath the container path that is the container id. + * @param project the project to bind + */ + @Override + public void initialize(IPath containerPath, IJavaProject project) throws CoreException { + if (CONTAINER_ID.equals(containerPath.toString())) { + JavaCore.setClasspathContainer(new Path(CONTAINER_ID), + new IJavaProject[] { project }, + new IClasspathContainer[] { allocateAndroidContainer(project) }, + new NullProgressMonitor()); + } + } + + /** + * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER} + * linking to the Android Framework. + */ + public static IClasspathEntry getContainerEntry() { + return JavaCore.newContainerEntry(new Path(CONTAINER_ID)); + } + + /** + * Checks the {@link IPath} objects against the android framework container id and + * returns true if they are identical. + * @param path the IPath to check. + */ + public static boolean checkPath(IPath path) { + return CONTAINER_ID.equals(path.toString()); + } + + /** + * Updates the {@link IJavaProject} objects with new android framework container. This forces + * JDT to recompile them. + * @param androidProjects the projects to update. + * @return true if success, false otherwise. + */ + public static boolean updateProjects(IJavaProject[] androidProjects) { + try { + // Allocate a new AndroidClasspathContainer, and associate it to the android framework + // container id for each projects. + // By providing a new association between a container id and a IClasspathContainer, + // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with + // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of + // the projects. + int projectCount = androidProjects.length; + + IClasspathContainer[] containers = new IClasspathContainer[projectCount]; + for (int i = 0 ; i < projectCount; i++) { + containers[i] = allocateAndroidContainer(androidProjects[i]); + } + + // give each project their new container in one call. + JavaCore.setClasspathContainer( + new Path(CONTAINER_ID), + androidProjects, containers, new NullProgressMonitor()); + + return true; + } catch (JavaModelException e) { + return false; + } + } + + /** + * Allocates and returns an {@link AndroidClasspathContainer} object with the proper + * path to the framework jar file. + * @param javaProject The java project that will receive the container. + */ + private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { + final IProject iProject = javaProject.getProject(); + + String markerMessage = null; + boolean outputToConsole = true; + + try { + AdtPlugin plugin = AdtPlugin.getDefault(); + + // get the lock object for project manipulation during SDK load. + Object lock = plugin.getSdkLockObject(); + synchronized (lock) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + IAndroidTarget target = null; + if (sdkIsLoaded) { + target = Sdk.getCurrent().getTarget(iProject); + } + + // if we are loaded and the target is non null, we create a valid ClassPathContainer + if (sdkIsLoaded && target != null) { + + String targetName = target.getClasspathName(); + + return new AndroidClasspathContainer( + createClasspathEntries(iProject, target, targetName), + new Path(CONTAINER_ID), targetName); + } + + // In case of error, we'll try different thing to provide the best error message + // possible. + // Get the project's target's hash string (if it exists) + String hashString = Sdk.getProjectTargetHashString(iProject); + + if (hashString == null || hashString.length() == 0) { + // if there is no hash string we only show this if the SDK is loaded. + // For a project opened at start-up with no target, this would be displayed + // twice, once when the project is opened, and once after the SDK has + // finished loading. + // By testing the sdk is loaded, we only show this once in the console. + if (sdkIsLoaded) { + markerMessage = String.format( + "Project has no target set. Edit the project properties to set one."); + } + } else if (sdkIsLoaded) { + markerMessage = String.format( + "Unable to resolve target '%s'", hashString); + } else { + // this is the case where there is a hashString but the SDK is not yet + // loaded and therefore we can't get the target yet. + // We check if there is a cache of the needed information. + AndroidClasspathContainer container = getContainerFromCache(iProject); + + if (container == null) { + // either the cache was wrong (ie folder does not exists anymore), or + // there was no cache. In this case we need to make sure the project + // is resolved again after the SDK is loaded. + plugin.setProjectToResolve(javaProject); + + markerMessage = String.format( + "Unable to resolve target '%s' until the SDK is loaded.", + hashString); + + // let's not log this one to the console as it will happen at every boot, + // and it's expected. (we do keep the error marker though). + outputToConsole = false; + + } else { + // we created a container from the cache, so we register the project + // to be checked for cache validity once the SDK is loaded + plugin.setProjectToCheck(javaProject); + + // and return the container + return container; + } + + } + + // return a dummy container to replace the one we may have had before. + // It'll be replaced by the real when if/when the target is resolved if/when the + // SDK finishes loading. + return new IClasspathContainer() { + public IClasspathEntry[] getClasspathEntries() { + return new IClasspathEntry[0]; + } + + public String getDescription() { + return "Unable to get system library for the project"; + } + + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + public IPath getPath() { + return null; + } + }; + } + } finally { + if (markerMessage != null) { + // log the error and put the marker on the project if we can. + if (outputToConsole) { + AdtPlugin.printErrorToConsole(iProject, markerMessage); + } + + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage, + -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); + } catch (CoreException e) { + // In some cases, the workspace may be locked for modification when we + // pass here. + // We schedule a new job to put the marker after. + final String fmessage = markerMessage; + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, + fmessage, -1, IMarker.SEVERITY_ERROR, + IMarker.PRIORITY_HIGH); + } catch (CoreException e2) { + return e2.getStatus(); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + } else { + // no error, remove potential MARKER_TARGETs. + try { + if (iProject.exists()) { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + // In some cases, the workspace may be locked for modification when we pass + // here, so we schedule a new job to put the marker after. + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } catch (CoreException e2) { + return e2.getStatus(); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + } + } + } + + /** + * Creates and returns an array of {@link IClasspathEntry} objects for the android + * framework and optional libraries. + *

This references the OS path to the android.jar and the + * java doc directory. This is dynamically created when a project is opened, + * and never saved in the project itself, so there's no risk of storing an + * obsolete path. + * The method also stores the paths used to create the entries in the project persistent + * properties. A new {@link AndroidClasspathContainer} can be created from the stored path + * using the {@link #getContainerFromCache(IProject)} method. + * @param project + * @param target The target that contains the libraries. + * @param targetName + */ + private static IClasspathEntry[] createClasspathEntries(IProject project, + IAndroidTarget target, String targetName) { + + // get the path from the target + String[] paths = getTargetPaths(target); + + // create the classpath entry from the paths + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + // paths now contains all the path required to recreate the IClasspathEntry with no + // target info. We encode them in a single string, with each path separated by + // OS path separator. + StringBuilder sb = new StringBuilder(CACHE_VERSION); + for (String p : paths) { + sb.append(PATH_SEPARATOR); + sb.append(p); + } + + // store this in a project persistent property + ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); + ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); + + return entries; + } + + /** + * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. + */ + private static AndroidClasspathContainer getContainerFromCache(IProject project) { + // get the cached info from the project persistent properties. + String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); + String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); + if (cache == null || targetNameCache == null) { + return null; + } + + // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. + if (cache.startsWith(CACHE_VERSION_SEP) == false) { + return null; + } + + cache = cache.substring(CACHE_VERSION_SEP.length()); + + // the cache contains multiple paths, separated by a character guaranteed to not be in + // the path (\u001C). + // The first 3 are for android.jar (jar, source, doc), the rest are for the optional + // libraries and should contain at least one doc and a jar (if there are any libraries). + // Therefore, the path count should be 3 or 5+ + String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (paths.length < 3 || paths.length == 4) { + return null; + } + + // now we check the paths actually exist. + // There's an exception: If the source folder for android.jar does not exist, this is + // not a problem, so we skip it. + // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a + // bit differently. + try { + if (new File(paths[CACHE_INDEX_JAR]).exists() == false || + new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { + return null; + } + + // check the path for the add-ons, if they exist. + if (paths.length > CACHE_INDEX_ADD_ON_START) { + + // check the docs path separately from the rest of the paths as it's a URI. + if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { + return null; + } + + // now just check the remaining paths. + for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { + String path = paths[i]; + if (path.length() > 0) { + File f = new File(path); + if (f.exists() == false) { + return null; + } + } + } + } + } catch (URISyntaxException e) { + return null; + } + + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + return new AndroidClasspathContainer(entries, + new Path(CONTAINER_ID), targetNameCache); + } + + /** + * Generates an array of {@link IClasspathEntry} from a set of paths. + * @see #getTargetPaths(IAndroidTarget) + */ + private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { + ArrayList list = new ArrayList(); + + // First, we create the IClasspathEntry for the framework. + // now add the android framework to the class path. + // create the path object. + IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); + IPath android_src = new Path(paths[CACHE_INDEX_SRC]); + + // create the java doc link. + IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + paths[CACHE_INDEX_DOCS_URI]); + + // create the access rule to restrict access to classes in com.android.internal + IAccessRule accessRule = JavaCore.newAccessRule( + new Path("com/android/internal/**"), //$NON-NLS-1$ + IAccessRule.K_NON_ACCESSIBLE); + + IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib, + android_src, // source attachment path + null, // default source attachment root path. + new IAccessRule[] { accessRule }, + new IClasspathAttribute[] { cpAttribute }, + false // not exported. + ); + + list.add(frameworkClasspathEntry); + + // now deal with optional libraries + if (paths.length >= 5) { + String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; + int i = 4; + while (i < paths.length) { + Path jarPath = new Path(paths[i++]); + + IClasspathAttribute[] attributes = null; + if (docPath.length() > 0) { + attributes = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + docPath) + }; + } + + IClasspathEntry entry = JavaCore.newLibraryEntry( + jarPath, + null, // source attachment path + null, // default source attachment root path. + null, + attributes, + false // not exported. + ); + list.add(entry); + } + } + + return list.toArray(new IClasspathEntry[list.size()]); + } + + /** + * Checks the projects' caches. If the cache was valid, the project is removed from the list. + * @param projects the list of projects to check. + */ + public static void checkProjectsCache(ArrayList projects) { + int i = 0; + projectLoop: while (i < projects.size()) { + IJavaProject javaProject = projects.get(i); + IProject iProject = javaProject.getProject(); + + // check if the project is opened + if (iProject.isOpen() == false) { + // remove from the list + // we do not increment i in this case. + projects.remove(i); + + continue; + } + + // get the target from the project and its paths + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + if (target == null) { + // this is really not supposed to happen. This would mean there are cached paths, + // but default.properties was deleted. Keep the project in the list to force + // a resolve which will display the error. + i++; + continue; + } + + String[] targetPaths = getTargetPaths(target); + + // now get the cached paths + String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); + if (cache == null) { + // this should not happen. We'll force resolve again anyway. + i++; + continue; + } + + String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (cachedPaths.length < 3 || cachedPaths.length == 4) { + // paths length is wrong. simply resolve the project again + i++; + continue; + } + + // Now we compare the paths. The first 4 can be compared directly. + // because of case sensitiveness we need to use File objects + + if (targetPaths.length != cachedPaths.length) { + // different paths, force resolve again. + i++; + continue; + } + + // compare the main paths (android.jar, main sources, main javadoc) + if (new File(targetPaths[CACHE_INDEX_JAR]).equals( + new File(cachedPaths[CACHE_INDEX_JAR])) == false || + new File(targetPaths[CACHE_INDEX_SRC]).equals( + new File(cachedPaths[CACHE_INDEX_SRC])) == false || + new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( + new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { + // compare optional libraries javadoc + if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( + new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + // testing the optional jar files is a little bit trickier. + // The order is not guaranteed to be identical. + // From a previous test, we do know however that there is the same number. + // The number of libraries should be low enough that we can simply go through the + // lists manually. + targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { + String targetPath = targetPaths[tpi]; + + // look for a match in the other array + for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { + if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { + // found a match. Try the next targetPath + continue targetLoop; + } + } + + // if we stop here, we haven't found a match, which means there's a + // discrepancy in the libraries. We force a resolve. + i++; + continue projectLoop; + } + } + + // at the point the check passes, and we can remove the project from the list. + // we do not increment i in this case. + projects.remove(i); + } + } + + /** + * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. + *

The paths are always in the same order. + *

    + *
  • Path to android.jar
  • + *
  • Path to the source code for android.jar
  • + *
  • Path to the javadoc for the android platform
  • + *
+ * Additionally, if there are optional libraries, the array will contain: + *
    + *
  • Path to the librairies javadoc
  • + *
  • Path to the first .jar file
  • + *
  • (more .jar as needed)
  • + *
+ */ + private static String[] getTargetPaths(IAndroidTarget target) { + ArrayList paths = new ArrayList(); + + // first, we get the path for android.jar + // The order is: android.jar, source folder, docs folder + paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + paths.add(target.getPath(IAndroidTarget.SOURCES)); + paths.add(AdtPlugin.getUrlDoc()); + + // now deal with optional libraries. + IOptionalLibrary[] libraries = target.getOptionalLibraries(); + if (libraries != null) { + // all the optional libraries use the same javadoc, so we start with this + String targetDocPath = target.getPath(IAndroidTarget.DOCS); + if (targetDocPath != null) { + paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); + } else { + // we add an empty string, to always have the same count. + paths.add(""); + } + + // because different libraries could use the same jar file, we make sure we add + // each jar file only once. + HashSet visitedJars = new HashSet(); + for (IOptionalLibrary library : libraries) { + String jarPath = library.getJarPath(); + if (visitedJars.contains(jarPath) == false) { + visitedJars.add(jarPath); + paths.add(jarPath); + } + } + } + + return paths.toArray(new String[paths.size()]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java new file mode 100644 index 0000000..70097ae --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java @@ -0,0 +1,291 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.build.ApkBuilder; +import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder; +import com.android.ide.eclipse.adt.internal.build.ResourceManagerBuilder; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.JavaCore; + +/** + * Project nature for the Android Projects. + */ +public class AndroidNature implements IProjectNature { + + /** the project this nature object is associated with */ + private IProject mProject; + + /** + * Configures this nature for its project. This is called by the workspace + * when natures are added to the project using + * IProject.setDescription and should not be called directly + * by clients. The nature extension id is added to the list of natures + * before this method is called, and need not be added here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * IProject.setDescription, but the nature will remain in + * the project description. + * + * The Android nature adds the pre-builder and the APK builder if necessary. + * + * @see org.eclipse.core.resources.IProjectNature#configure() + * @throws CoreException if configuration fails. + */ + public void configure() throws CoreException { + configureResourceManagerBuilder(mProject); + configurePreBuilder(mProject); + configureApkBuilder(mProject); + } + + /** + * De-configures this nature for its project. This is called by the + * workspace when natures are removed from the project using + * IProject.setDescription and should not be called directly + * by clients. The nature extension id is removed from the list of natures + * before this method is called, and need not be removed here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * IProject.setDescription, but the nature will still be + * removed from the project description. + * + * The Android nature removes the custom pre builder and APK builder. + * + * @see org.eclipse.core.resources.IProjectNature#deconfigure() + * @throws CoreException if configuration fails. + */ + public void deconfigure() throws CoreException { + // remove the android builders + removeBuilder(mProject, ResourceManagerBuilder.ID); + removeBuilder(mProject, PreCompilerBuilder.ID); + removeBuilder(mProject, ApkBuilder.ID); + } + + /** + * Returns the project to which this project nature applies. + * + * @return the project handle + * @see org.eclipse.core.resources.IProjectNature#getProject() + */ + public IProject getProject() { + return mProject; + } + + /** + * Sets the project to which this nature applies. Used when instantiating + * this project nature runtime. This is called by + * IProject.create() or + * IProject.setDescription() and should not be called + * directly by clients. + * + * @param project the project to which this nature applies + * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject) + */ + public void setProject(IProject project) { + mProject = project; + } + + /** + * Adds the Android Nature and the Java Nature to the project if it doesn't + * already have them. + * + * @param project An existing or new project to update + * @param monitor An optional progress monitor. Can be null. + * @throws CoreException if fails to change the nature. + */ + public static synchronized void setupProjectNatures(IProject project, + IProgressMonitor monitor) throws CoreException { + if (project == null || !project.isOpen()) return; + if (monitor == null) monitor = new NullProgressMonitor(); + + // Add the natures. We need to add the Java nature first, so it adds its builder to the + // project first. This way, when the android nature is added, we can control where to put + // the android builders in relation to the java builder. + // Adding the java nature after the android one, would place the java builder before the + // android builders. + addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor); + addNatureToProjectDescription(project, AndroidConstants.NATURE, monitor); + } + + /** + * Add the specified nature to the specified project. The nature is only + * added if not already present. + *

+ * Android Natures are always inserted at the beginning of the list of natures in order to + * have the jdt views/dialogs display the proper icon. + * + * @param project The project to modify. + * @param natureId The Id of the nature to add. + * @param monitor An existing progress monitor. + * @throws CoreException if fails to change the nature. + */ + private static void addNatureToProjectDescription(IProject project, + String natureId, IProgressMonitor monitor) throws CoreException { + if (!project.hasNature(natureId)) { + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + String[] newNatures = new String[natures.length + 1]; + + // Android natures always come first. + if (natureId.equals(AndroidConstants.NATURE)) { + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = natureId; + } else { + System.arraycopy(natures, 0, newNatures, 0, natures.length); + newNatures[natures.length] = natureId; + } + + description.setNatureIds(newNatures); + project.setDescription(description, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the ResourceManagerBuilder, if its not already there. It'll insert + * itself as the first builder. + * @throws CoreException + * + */ + public static void configureResourceManagerBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // it's not there, lets add it at the beginning of the builders + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 1, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(ResourceManagerBuilder.ID); + newCommands[0] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Adds the PreCompilerBuilder if its not already there. It'll check for + * presence of the ResourceManager and insert itself right after. + * @param project + * @throws CoreException + */ + public static void configurePreBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (PreCompilerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // we need to add it after the resource manager builder. + // Let's look for it + int index = -1; + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + index = i; + break; + } + } + + // we're inserting after + index++; + + // do the insertion + + // copy the builders before. + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, index); + + // insert the new builder + ICommand command = desc.newCommand(); + command.setBuilderName(PreCompilerBuilder.ID); + newCommands[index] = command; + + // copy the builder after + System.arraycopy(commands, index, newCommands, index + 1, commands.length-index); + + // set the new builders in the project + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + public static void configureApkBuilder(IProject project) + throws CoreException { + // Add the .apk builder at the end if it's not already there + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + for (int i = 0; i < commands.length; ++i) { + if (ApkBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(ApkBuilder.ID); + newCommands[commands.length] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Removes a builder from the project. + * @param project The project to remove the builder from. + * @param id The String ID of the builder to remove. + * @return true if the builder was found and removed. + * @throws CoreException + */ + public static boolean removeBuilder(IProject project, String id) throws CoreException { + IProjectDescription description = project.getDescription(); + ICommand[] commands = description.getBuildSpec(); + for (int i = 0; i < commands.length; ++i) { + if (id.equals(commands[i].getBuilderName())) { + ICommand[] newCommands = new ICommand[commands.length - 1]; + System.arraycopy(commands, 0, newCommands, 0, i); + System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1); + description.setBuildSpec(newCommands); + project.setDescription(description, null); + return true; + } + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java new file mode 100644 index 0000000..4182e8e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ApkInstallManager.java @@ -0,0 +1,207 @@ +/* + * 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.project; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Device; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; + +import org.eclipse.core.resources.IProject; + +import java.util.ArrayList; + +/** + * Registers which apk was installed on which device. + *

+ * The goal of this class is to remember the installation of APKs on devices, and provide + * information about whether a new APK should be installed on a device prior to running the + * application from a launch configuration. + *

+ * The manager uses {@link IProject} and {@link IDevice} to identify the target device and the + * (project generating the) APK. This ensures that disconnected and reconnected devices will + * always receive new APKs (since the APK could be uninstalled manually). + *

+ * Manually uninstalling an APK from a connected device will still be a problem, but this should + * be a limited use case. + *

+ * This is a singleton. To get the instance, use {@link #getInstance()} + */ +public class ApkInstallManager implements IDeviceChangeListener, IDebugBridgeChangeListener, + IProjectListener { + + private final static ApkInstallManager sThis = new ApkInstallManager(); + + /** + * Internal struct to associate a project and a device. + */ + private static class ApkInstall { + public ApkInstall(IProject project, IDevice device) { + this.project = project; + this.device = device; + } + IProject project; + IDevice device; + } + + private final ArrayList mInstallList = new ArrayList(); + + public static ApkInstallManager getInstance() { + return sThis; + } + + /** + * Registers an installation of project onto device + * @param project The project that was installed. + * @param device The device that received the installation. + */ + public void registerInstallation(IProject project, IDevice device) { + synchronized (mInstallList) { + mInstallList.add(new ApkInstall(project, device)); + } + } + + /** + * Returns whether a project was installed on the device. + * @param project the project that may have been installed. + * @param device the device that may have received the installation. + * @return + */ + public boolean isApplicationInstalled(IProject project, IDevice device) { + synchronized (mInstallList) { + for (ApkInstall install : mInstallList) { + if (project == install.project && device == install.device) { + return true; + } + } + } + return false; + } + + /** + * Resets registered installations for a specific {@link IProject}. + *

This ensures that {@link #isApplicationInstalled(IProject, IDevice)} will always return + * null for this specified project, for any device. + * @param project the project for which to reset all installations. + */ + public void resetInstallationFor(IProject project) { + synchronized (mInstallList) { + for (int i = 0 ; i < mInstallList.size() ;) { + ApkInstall install = mInstallList.get(i); + if (install.project == project) { + mInstallList.remove(i); + } else { + i++; + } + } + } + } + + private ApkInstallManager() { + AndroidDebugBridge.addDeviceChangeListener(this); + AndroidDebugBridge.addDebugBridgeChangeListener(this); + ResourceMonitor.getMonitor().addProjectListener(this); + } + + /* + * Responds to a bridge change by clearing the full installation list. + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener#bridgeChanged(com.android.ddmlib.AndroidDebugBridge) + */ + public void bridgeChanged(AndroidDebugBridge bridge) { + // the bridge changed, there is no way to know which IDevice will be which. + // We reset everything + synchronized (mInstallList) { + mInstallList.clear(); + } + } + + /* + * Responds to a device being disconnected by removing all installations related to this device. + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceDisconnected(com.android.ddmlib.Device) + */ + public void deviceDisconnected(Device device) { + synchronized (mInstallList) { + for (int i = 0 ; i < mInstallList.size() ;) { + ApkInstall install = mInstallList.get(i); + if (install.device == device) { + mInstallList.remove(i); + } else { + i++; + } + } + } + } + + /* + * Responds to a close project by resetting all its installation. + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectClosed(org.eclipse.core.resources.IProject) + */ + public void projectClosed(IProject project) { + resetInstallationFor(project); + } + + /* + * Responds to a close project by resetting all its installation. + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectDeleted(org.eclipse.core.resources.IProject) + */ + public void projectDeleted(IProject project) { + resetInstallationFor(project); + } + + /* + * Does nothing + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceChanged(com.android.ddmlib.Device, int) + */ + public void deviceChanged(Device device, int changeMask) { + // nothing to do. + } + + /* + * Does nothing + * (non-Javadoc) + * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceConnected(com.android.ddmlib.Device) + */ + public void deviceConnected(Device device) { + // nothing to do. + } + + /* + * Does nothing + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectOpened(org.eclipse.core.resources.IProject) + */ + public void projectOpened(IProject project) { + // nothing to do. + } + + /* + * Does nothing + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectOpenedWithWorkspace(org.eclipse.core.resources.IProject) + */ + public void projectOpenedWithWorkspace(IProject project) { + // nothing to do. + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java new file mode 100644 index 0000000..e311bfb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FixLaunchConfig.java @@ -0,0 +1,156 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; + +import java.util.ArrayList; + +/** + * Class to fix the launch configuration of a project if the java package + * defined in the manifest has been changed.
+ * This fix can be done synchronously, or asynchronously.
+ * start() will start a thread that will do the fix.
+ * run() will do the fix in the current thread.

+ * By default, the fix first display a dialog to the user asking if he/she wants to + * do the fix. This can be overriden by calling setDisplayPrompt(false). + * + */ +public class FixLaunchConfig extends Thread { + + private IProject mProject; + private String mOldPackage; + private String mNewPackage; + + private boolean mDisplayPrompt = true; + + public FixLaunchConfig(IProject project, String oldPackage, String newPackage) { + super(); + + mProject = project; + mOldPackage = oldPackage; + mNewPackage = newPackage; + } + + /** + * Set the display prompt. If true run()/start() first ask the user if he/she wants + * to fix the Launch Config + * @param displayPrompt + */ + public void setDisplayPrompt(boolean displayPrompt) { + mDisplayPrompt = displayPrompt; + } + + /** + * Fix the Launch configurations. + */ + @Override + public void run() { + + if (mDisplayPrompt) { + // ask the user if he really wants to fix the launch config + boolean res = AdtPlugin.displayPrompt( + "Launch Configuration Update", + "The package definition in the manifest changed.\nDo you want to update your Launch Configuration(s)?"); + + if (res == false) { + return; + } + } + + // get the list of config for the project + String projectName = mProject.getName(); + ILaunchConfiguration[] configs = findConfigs(mProject.getName()); + + // loop through all the config and update the package + for (ILaunchConfiguration config : configs) { + try { + // get the working copy so that we can make changes. + ILaunchConfigurationWorkingCopy copy = config.getWorkingCopy(); + + // get the attributes for the activity + String activity = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, + ""); //$NON-NLS-1$ + + // manifests can define activities that are not in the defined package, + // so we need to make sure the activity is inside the old package. + if (activity.startsWith(mOldPackage)) { + // create the new activity + activity = mNewPackage + activity.substring(mOldPackage.length()); + + // put it in the copy + copy.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, activity); + + // save the config + copy.doSave(); + } + } catch (CoreException e) { + // couldn't get the working copy. we output the error in the console + String msg = String.format("Failed to modify %1$s: %2$s", projectName, + e.getMessage()); + AdtPlugin.printErrorToConsole(mProject, msg); + } + } + + } + + /** + * Looks for and returns all existing Launch Configuration object for a + * specified project. + * @param projectName The name of the project + * @return all the ILaunchConfiguration object. If none are present, an empty array is + * returned. + */ + private static ILaunchConfiguration[] findConfigs(String projectName) { + // get the launch manager + ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); + + // now get the config type for our particular android type. + ILaunchConfigurationType configType = manager. + getLaunchConfigurationType(LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); + + // create a temp list to hold all the valid configs + ArrayList list = new ArrayList(); + + try { + ILaunchConfiguration[] configs = manager.getLaunchConfigurations(configType); + + for (ILaunchConfiguration config : configs) { + if (config.getAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + "").equals(projectName)) { //$NON-NLS-1$ + list.add(config); + } + } + } catch (CoreException e) { + } + + return list.toArray(new ILaunchConfiguration[list.size()]); + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java new file mode 100644 index 0000000..5f4e22e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/FolderDecorator.java @@ -0,0 +1,107 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.jface.viewers.ILabelDecorator; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ILightweightLabelDecorator; + +/** + * A {@link ILabelDecorator} associated with an org.eclipse.ui.decorators extension. + * This is used to add android icons in some special folders in the package explorer. + */ +public class FolderDecorator implements ILightweightLabelDecorator { + + private ImageDescriptor mDescriptor; + + public FolderDecorator() { + mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); //$NON-NLS-1$ + } + + public void decorate(Object element, IDecoration decoration) { + if (element instanceof IFolder) { + IFolder folder = (IFolder)element; + + // get the project and make sure this is an android project + IProject project = folder.getProject(); + + try { + if (project.hasNature(AndroidConstants.NATURE)) { + // check the folder is directly under the project. + if (folder.getParent().getType() == IResource.PROJECT) { + String name = folder.getName(); + if (name.equals(SdkConstants.FD_ASSETS)) { + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_RESOURCES)) { + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_GEN_SOURCES)) { + doDecoration(decoration, " [Generated Java Files]"); + } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { + doDecoration(decoration, null); + } + } + } + } catch (CoreException e) { + // log the error + AdtPlugin.log(e, "Unable to get nature of project '%s'.", project.getName()); + } + } + } + + public void doDecoration(IDecoration decoration, String suffix) { + decoration.addOverlay(mDescriptor, IDecoration.TOP_LEFT); + + if (suffix != null) { + decoration.addSuffix(suffix); + + // this is broken as it changes the color of the whole text, not only of the decoration. + // TODO: figure out how to change the color of the decoration only. +// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); +// ColorRegistry registry = theme.getColorRegistry(); +// decoration.setForegroundColor( +// registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations")); //$NON-NLS-1$ + } + + } + + public boolean isLabelProperty(Object element, String property) { + // Property change do not affect the label + return false; + } + + public void addListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + public void removeListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + public void dispose() { + // nothing to dispose + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java new file mode 100644 index 0000000..4d55bd4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectHelper.java @@ -0,0 +1,770 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.JavaRuntime; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class to manipulate Project parameters/properties. + */ +public final class ProjectHelper { + public final static int COMPILER_COMPLIANCE_OK = 0; + public final static int COMPILER_COMPLIANCE_LEVEL = 1; + public final static int COMPILER_COMPLIANCE_SOURCE = 2; + public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3; + + /** + * Adds the corresponding source folder to the class path entries. + * + * @param entries The class path entries to read. A copy will be returned. + * @param new_entry The parent source folder to remove. + * @return A new class path entries array. + */ + public static IClasspathEntry[] addEntryToClasspath( + IClasspathEntry[] entries, IClasspathEntry new_entry) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n + 1]; + System.arraycopy(entries, 0, newEntries, 0, n); + newEntries[n] = new_entry; + return newEntries; + } + + /** + * Adds the corresponding source folder to the project's class path entries. + * + * @param javaProject The java project of which path entries to update. + * @param new_entry The parent source folder to remove. + * @throws JavaModelException + */ + public static void addEntryToClasspath( + IJavaProject javaProject, IClasspathEntry new_entry) + throws JavaModelException { + + IClasspathEntry[] entries = javaProject.getRawClasspath(); + entries = addEntryToClasspath(entries, new_entry); + javaProject.setRawClasspath(entries, new NullProgressMonitor()); + } + + /** + * Remove a classpath entry from the array. + * @param entries The class path entries to read. A copy will be returned + * @param index The index to remove. + * @return A new class path entries array. + */ + public static IClasspathEntry[] removeEntryFromClasspath( + IClasspathEntry[] entries, int index) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n-1]; + + // copy the entries before index + System.arraycopy(entries, 0, newEntries, 0, index); + + // copy the entries after index + System.arraycopy(entries, index + 1, newEntries, index, + entries.length - index - 1); + + return newEntries; + } + + /** + * Converts a OS specific path into a path valid for the java doc location + * attributes of a project. + * @param javaDocOSLocation The OS specific path. + * @return a valid path for the java doc location. + */ + public static String getJavaDocPath(String javaDocOSLocation) { + // first thing we do is convert the \ into / + String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$ + AndroidConstants.WS_SEP); + + // then we add file: at the beginning for unix path, and file:/ for non + // unix path + if (javaDoc.startsWith(AndroidConstants.WS_SEP)) { + return "file:" + javaDoc; //$NON-NLS-1$ + } + + return "file:/" + javaDoc; //$NON-NLS-1$ + } + + /** + * Look for a specific classpath entry by full path and return its index. + * @param entries The entry array to search in. + * @param entryPath The OS specific path of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByPath(IClasspathEntry[] entries, + String entryPath, int entryKind) { + for (int i = 0 ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + + String osPathString = path.toOSString(); + if (osPathString.equals(entryPath)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + /** + * Look for a specific classpath entry for file name only and return its + * index. + * @param entries The entry array to search in. + * @param entryName The filename of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @param startIndex Index where to start the search + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByName(IClasspathEntry[] entries, + String entryName, int entryKind, int startIndex) { + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + String name = path.segment(path.segmentCount()-1); + + if (name.equals(entryName)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + /** + * Fix the project. This checks the SDK location. + * @param project The project to fix. + * @throws JavaModelException + */ + public static void fixProject(IProject project) throws JavaModelException { + if (AdtPlugin.getOsSdkFolder().length() == 0) { + AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed."); + return; + } + + // get a java project + IJavaProject javaProject = JavaCore.create(project); + fixProjectClasspathEntries(javaProject); + } + + /** + * Fix the project classpath entries. The method ensures that: + *

    + *
  • The project does not reference any old android.zip/android.jar archive.
  • + *
  • The project does not use its output folder as a sourc folder.
  • + *
  • The project does not reference a desktop JRE
  • + *
  • The project references the AndroidClasspathContainer. + *
+ * @param javaProject The project to fix. + * @throws JavaModelException + */ + public static void fixProjectClasspathEntries(IJavaProject javaProject) + throws JavaModelException { + + // get the project classpath + IClasspathEntry[] entries = javaProject.getRawClasspath(); + IClasspathEntry[] oldEntries = entries; + + // check if the JRE is set as library + int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER, + IClasspathEntry.CPE_CONTAINER); + if (jreIndex != -1) { + // the project has a JRE included, we remove it + entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex); + } + + // get the output folder + IPath outputFolder = javaProject.getOutputLocation(); + + boolean foundContainer = false; + + for (int i = 0 ; i < entries.length ;) { + // get the entry and kind + IClasspathEntry entry = entries[i]; + int kind = entry.getEntryKind(); + + if (kind == IClasspathEntry.CPE_SOURCE) { + IPath path = entry.getPath(); + + if (path.equals(outputFolder)) { + entries = ProjectHelper.removeEntryFromClasspath(entries, i); + + // continue, to skip the i++; + continue; + } + } else if (kind == IClasspathEntry.CPE_CONTAINER) { + if (AndroidClasspathContainerInitializer.checkPath(entry.getPath())) { + foundContainer = true; + } + } + + i++; + } + + // if the framework container is not there, we add it + if (foundContainer == false) { + // add the android container to the array + entries = ProjectHelper.addEntryToClasspath(entries, + AndroidClasspathContainerInitializer.getContainerEntry()); + } + + // set the new list of entries to the project + if (entries != oldEntries) { + javaProject.setRawClasspath(entries, new NullProgressMonitor()); + } + + // If needed, check and fix compiler compliance and source compatibility + ProjectHelper.checkAndFixCompilerCompliance(javaProject); + } + + + /** + * Checks the project compiler compliance level is supported. + * @param javaProject The project to check + * @return
    + *
  • COMPILER_COMPLIANCE_OK if the project is properly configured
  • + *
  • COMPILER_COMPLIANCE_LEVEL for unsupported compiler level
  • + *
  • COMPILER_COMPLIANCE_SOURCE for unsupported source compatibility
  • + *
  • COMPILER_COMPLIANCE_CODEGEN_TARGET for unsupported .class format
  • + *
+ */ + public static final int checkCompilerCompliance(IJavaProject javaProject) { + // get the project compliance level option + String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(compliance) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_LEVEL; + } + + // otherwise we check source compatibility + String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(source) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_SOURCE; + } + + // otherwise check codegen level + String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(codeGen) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_CODEGEN_TARGET; + } + + return COMPILER_COMPLIANCE_OK; + } + + /** + * Checks the project compiler compliance level is supported. + * @param project The project to check + * @return
    + *
  • COMPILER_COMPLIANCE_OK if the project is properly configured
  • + *
  • COMPILER_COMPLIANCE_LEVEL for unsupported compiler level
  • + *
  • COMPILER_COMPLIANCE_SOURCE for unsupported source compatibility
  • + *
  • COMPILER_COMPLIANCE_CODEGEN_TARGET for unsupported .class format
  • + *
+ */ + public static final int checkCompilerCompliance(IProject project) { + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // check and return the result. + return checkCompilerCompliance(javaProject); + } + + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param project The project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IProject project) { + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // Now we check the compiler compliance level and make sure it is valid + checkAndFixCompilerCompliance(javaProject); + } + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param javaProject The Java project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) { + if (checkCompilerCompliance(javaProject) != COMPILER_COMPLIANCE_OK) { + // setup the preferred compiler compliance level. + javaProject.setOption(JavaCore.COMPILER_COMPLIANCE, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_SOURCE, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + + // clean the project to make sure we recompile + try { + javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD, + new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.printErrorToConsole(javaProject.getProject(), + "Project compiler settings changed. Clean your project."); + } + } + } + + /** + * Returns a {@link IProject} by its running application name, as it returned by the AVD. + *

+ * applicationName will in most case be the package declared in the manifest, but + * can, in some cases, be a custom process name declared in the manifest, in the + * application, activity, receiver, or + * service nodes. + * @param applicationName The application name. + * @return a project or null if no matching project were found. + */ + public static IProject findAndroidProjectByAppName(String applicationName) { + // Get the list of project for the current workspace + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject[] projects = workspace.getRoot().getProjects(); + + // look for a project that matches the packageName of the app + // we're trying to debug + for (IProject p : projects) { + if (p.isOpen()) { + try { + if (p.hasNature(AndroidConstants.NATURE) == false) { + // ignore non android projects + continue; + } + } catch (CoreException e) { + // failed to get the nature? skip project. + continue; + } + + // check that there is indeed a manifest file. + IFile manifestFile = AndroidManifestParser.getManifest(p); + if (manifestFile == null) { + // no file? skip this project. + continue; + } + + AndroidManifestParser parser = null; + try { + parser = AndroidManifestParser.parseForData(manifestFile); + } catch (CoreException e) { + // ignore, handled below. + } + if (parser == null) { + // skip this project. + continue; + } + + String manifestPackage = parser.getPackage(); + + if (manifestPackage != null && manifestPackage.equals(applicationName)) { + // this is the project we were looking for! + return p; + } else { + // if the package and application name don't match, + // we look for other possible process names declared in the manifest. + String[] processes = parser.getProcesses(); + for (String process : processes) { + if (process.equals(applicationName)) { + return p; + } + } + } + } + } + + return null; + + } + + public static void fixProjectNatureOrder(IProject project) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // if the android nature is not the first one, we reorder them + if (AndroidConstants.NATURE.equals(natures[0]) == false) { + // look for the index + for (int i = 0 ; i < natures.length ; i++) { + if (AndroidConstants.NATURE.equals(natures[i])) { + // if we try to just reorder the array in one pass, this doesn't do + // anything. I guess JDT check that we are actually adding/removing nature. + // So, first we'll remove the android nature, and then add it back. + + // remove the android nature + removeNature(project, AndroidConstants.NATURE); + + // now add it back at the first index. + description = project.getDescription(); + natures = description.getNatureIds(); + + String[] newNatures = new String[natures.length + 1]; + + // first one is android + newNatures[0] = AndroidConstants.NATURE; + + // next the rest that was before the android nature + System.arraycopy(natures, 0, newNatures, 1, natures.length); + + // set the new natures + description.setNatureIds(newNatures); + project.setDescription(description, null); + + // and stop + break; + } + } + } + } + + + /** + * Removes a specific nature from a project. + * @param project The project to remove the nature from. + * @param nature The nature id to remove. + * @throws CoreException + */ + public static void removeNature(IProject project, String nature) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (nature.equals(natures[i])) { + String[] newNatures = new String[natures.length - 1]; + if (i > 0) { + System.arraycopy(natures, 0, newNatures, 0, i); + } + System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1); + description.setNatureIds(newNatures); + project.setDescription(description, null); + + return; + } + } + + } + + /** + * Returns if the project has error level markers. + * @param includeReferencedProjects flag to also test the referenced projects. + * @throws CoreException + */ + public static boolean hasError(IProject project, boolean includeReferencedProjects) + throws CoreException { + IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); + if (markers != null && markers.length > 0) { + // the project has marker(s). even though they are "problem" we + // don't know their severity. so we loop on them and figure if they + // are warnings or errors + for (IMarker m : markers) { + int s = m.getAttribute(IMarker.SEVERITY, -1); + if (s == IMarker.SEVERITY_ERROR) { + return true; + } + } + } + + // test the referenced projects if needed. + if (includeReferencedProjects) { + IProject[] projects = getReferencedProjects(project); + + for (IProject p : projects) { + if (hasError(p, false)) { + return true; + } + } + } + + return false; + } + + /** + * Saves a String property into the persistent storage of a resource. + * @param resource The resource into which the string value is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveStringProperty(IResource resource, String propertyName, + String value) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + resource.setPersistentProperty(qname, value); + } catch (CoreException e) { + return false; + } + + return true; + } + + /** + * Loads a String property from the persistent storage of a resource. + * @param resource The resource from which the string value is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return the property value or null if it was not found. + */ + public static String loadStringProperty(IResource resource, String propertyName) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + String value = resource.getPersistentProperty(qname); + return value; + } catch (CoreException e) { + return null; + } + } + + /** + * Saves a property into the persistent storage of a resource. + * @param resource The resource into which the boolean value is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveBooleanProperty(IResource resource, String propertyName, + boolean value) { + return saveStringProperty(resource, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of the project. + * @param resource The resource from which the boolean value is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + public static boolean loadBooleanProperty(IResource resource, String propertyName, + boolean defaultValue) { + String value = loadStringProperty(resource, propertyName); + if (value != null) { + return Boolean.parseBoolean(value); + } + + return defaultValue; + } + + /** + * Saves the path of a resource into the persistent storate of the project. + * @param resource The resource into which the resource path is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value The resource to save. It's its path that is actually stored. If null, an + * empty string is stored. + * @return true if the save succeeded + */ + public static boolean saveResourceProperty(IResource resource, String propertyName, + IResource value) { + if (value != null) { + IPath iPath = value.getProjectRelativePath(); + return saveStringProperty(resource, propertyName, iPath.toString()); + } + + return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$ + } + + /** + * Loads the path of a resource from the persistent storage of the project, and returns the + * corresponding IResource object, if it exists in the same project as resource. + * @param resource The resource from which the resource path is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return The corresponding IResource object (or children interface) or null + */ + public static IResource loadResourceProperty(IResource resource, String propertyName) { + String value = loadStringProperty(resource, propertyName); + + if (value != null && value.length() > 0) { + return resource.getProject().findMember(value); + } + + return null; + } + + /** + * Returns the list of referenced project that are opened and Java projects. + * @param project + * @return list of opened referenced java project. + * @throws CoreException + */ + public static IProject[] getReferencedProjects(IProject project) throws CoreException { + IProject[] projects = project.getReferencedProjects(); + + ArrayList list = new ArrayList(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + list.add(p); + } + } + + return list.toArray(new IProject[list.size()]); + } + + + /** + * Checks a Java project compiler level option against a list of supported versions. + * @param optionValue the Compiler level option. + * @return true if the option value is supproted. + */ + private static boolean checkCompliance(String optionValue) { + for (String s : AndroidConstants.COMPILER_COMPLIANCE) { + if (s != null && s.equals(optionValue)) { + return true; + } + } + + return false; + } + + /** + * Returns the apk filename for the given project + * @param project The project. + * @param config An optional config name. Can be null. + */ + public static String getApkFilename(IProject project, String config) { + if (config != null) { + return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$ + } + + return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + } + + /** + * Find the list of projects on which this JavaProject is dependent on at the compilation level. + * + * @param javaProject Java project that we are looking for the dependencies. + * @return A list of Java projects for which javaProject depend on. + * @throws JavaModelException + */ + public static List getAndroidProjectDependencies(IJavaProject javaProject) + throws JavaModelException { + String[] requiredProjectNames = javaProject.getRequiredProjectNames(); + + // Go from java project name to JavaProject name + IJavaModel javaModel = javaProject.getJavaModel(); + + // loop through all dependent projects and keep only those that are Android projects + List projectList = new ArrayList(requiredProjectNames.length); + for (String javaProjectName : requiredProjectNames) { + IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName); + + //Verify that the project has also the Android Nature + try { + if (!androidJavaProject.getProject().hasNature(AndroidConstants.NATURE)) { + continue; + } + } catch (CoreException e) { + continue; + } + + projectList.add(androidJavaProject); + } + + return projectList; + } + + /** + * Returns the android package file as an IFile object for the specified + * project. + * @param project The project + * @return The android package as an IFile object or null if not found. + */ + public static IFile getApplicationPackage(IProject project) { + // get the output folder + IFolder outputLocation = BaseProjectHelper.getOutputFolder(project); + + if (outputLocation == null) { + AdtPlugin.printErrorToConsole(project, + "Failed to get the output location of the project. Check build path properties" + ); + return null; + } + + + // get the package path + String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + IResource r = outputLocation.findMember(packageName); + + // check the package is present + if (r instanceof IFile && r.exists()) { + return (IFile)r; + } + + String msg = String.format("Could not find %1$s!", packageName); + AdtPlugin.printErrorToConsole(project, msg); + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java new file mode 100644 index 0000000..0af0866 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java @@ -0,0 +1,123 @@ +/* + * 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.properties; + +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.sdkuilib.ApkConfigWidget; +import com.android.sdkuilib.SdkTargetSelector; + +import org.eclipse.core.resources.IProject; +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.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.IWorkbenchPropertyPage; +import org.eclipse.ui.dialogs.PropertyPage; + +import java.util.Map; + +/** + * Property page for "Android" project. + * This is accessible from the Package Explorer when right clicking a project and choosing + * "Properties". + * + */ +public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPropertyPage { + + private IProject mProject; + private SdkTargetSelector mSelector; + private ApkConfigWidget mApkConfigWidget; + + public AndroidPropertyPage() { + // pass + } + + @Override + protected Control createContents(Composite parent) { + // get the element (this is not yet valid in the constructor). + mProject = (IProject)getElement(); + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + + // build the UI. + Composite top = new Composite(parent, SWT.NONE); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + top.setLayout(new GridLayout(1, false)); + + Label l = new Label(top, SWT.NONE); + l.setText("Project Build Target"); + + mSelector = new SdkTargetSelector(top, targets); + + l = new Label(top, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + l = new Label(top, SWT.NONE); + l.setText("Project APK Configurations"); + + mApkConfigWidget = new ApkConfigWidget(top); + + // fill the ui + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null && mProject.isOpen()) { + // get the target + IAndroidTarget target = currentSdk.getTarget(mProject); + if (target != null) { + mSelector.setSelection(target); + } + + // get the apk configurations + Map configs = currentSdk.getProjectApkConfigs(mProject); + mApkConfigWidget.fillTable(configs); + } + + mSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // look for the selection and validate the page if there is a selection + IAndroidTarget target = mSelector.getSelected(); + setValid(target != null); + } + }); + + if (mProject.isOpen() == false) { + // disable the ui. + } + + return top; + } + + @Override + public boolean performOk() { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + currentSdk.setProject(mProject, mSelector.getSelected(), + mApkConfigWidget.getApkConfigs()); + } + + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java new file mode 100644 index 0000000..668cff3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java @@ -0,0 +1,170 @@ +/* + * 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.refactorings.extractstring; + +import com.android.ide.eclipse.adt.AndroidConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.FileEditorInput; + +/* + * Quick Reference Link: + * http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html + * and + * http://www.ibm.com/developerworks/opensource/library/os-ecjdt/ + */ + +/** + * Action executed when the "Extract String" menu item is invoked. + *

+ * The intent of the action is to start a refactoring that extracts a source string and + * replaces it by an Android string resource ID. + *

+ * Workflow: + *

    + *
  • The action is currently located in the Refactoring menu in the main menu. + *
  • TODO: extend the popup refactoring menu in a Java or Android XML file. + *
  • The action is only enabled if the selection is 1 character or more. That is at least part + * of the string must be selected, it's not enough to just move the insertion point. This is + * a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when + * the insertion point is merely moved. TODO: address this limitation. + *
      The action gets the current {@link ISelection}. It also knows the current + * {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the + * actual resource file. By looking at the Active Window > Active Page > Active Editor we + * can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file) + * that is being edited. + *
        TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit} + * can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow + * us to be able to work with a selection from an Android XML file later. + *
      • The action creates a new {@link ExtractStringRefactoring} and make it run on in a new + * {@link ExtractStringWizard}. + *
          + */ +public class ExtractStringAction implements IWorkbenchWindowActionDelegate { + + /** Keep track of the current workbench window. */ + private IWorkbenchWindow mWindow; + private ITextSelection mSelection; + private IFile mFile; + + /** + * Keep track of the current workbench window. + */ + public void init(IWorkbenchWindow window) { + mWindow = window; + } + + public void dispose() { + // Nothing to do + } + + /** + * Examine the selection to determine if the action should be enabled or not. + *

          + * Keep a link to the relevant selection structure (i.e. a part of the Java AST). + */ + public void selectionChanged(IAction action, ISelection selection) { + + // Note, two kinds of selections are returned here: + // ITextSelection on a Java source window + // IStructuredSelection in the outline or navigator + // This simply deals with the refactoring based on a non-empty selection. + // At that point, just enable the action and later decide if it's valid when it actually + // runs since we don't have access to the AST yet. + + mSelection = null; + mFile = null; + + if (selection instanceof ITextSelection) { + mSelection = (ITextSelection) selection; + if (mSelection.getLength() > 0) { + mFile = getSelectedFile(); + } + } + + action.setEnabled(mSelection != null && mFile != null); + } + + /** + * Create a new instance of our refactoring and a wizard to configure it. + */ + public void run(IAction action) { + if (mSelection != null && mFile != null) { + ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mSelection); + RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject()); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + op.run(mWindow.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + // Interrupted. Pass. + } + } + } + + /** + * Returns the active {@link IFile} (hopefully matching our selection) or null. + * The file is only returned if it's a file from a project with an Android nature. + *

          + * At that point we do not try to analyze if the selection nor the file is suitable + * for the refactoring. This check is performed when the refactoring is invoked since + * it can then produce meaningful error messages as needed. + */ + private IFile getSelectedFile() { + IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (wwin != null) { + IWorkbenchPage page = wwin.getActivePage(); + if (page != null) { + IEditorPart editor = page.getActiveEditor(); + if (editor != null) { + IEditorInput input = editor.getEditorInput(); + + if (input instanceof FileEditorInput) { + FileEditorInput fi = (FileEditorInput) input; + IFile file = fi.getFile(); + if (file.exists()) { + IProject proj = file.getProject(); + try { + if (proj != null && proj.hasNature(AndroidConstants.NATURE)) { + return file; + } + } catch (CoreException e) { + // ignore + } + } + } + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java new file mode 100644 index 0000000..14fd506 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java @@ -0,0 +1,53 @@ +/* + * 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.refactorings.extractstring; + +import org.eclipse.ltk.core.refactoring.RefactoringContribution; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; + +import java.util.Map; + +/** + * @see ExtractStringDescriptor + */ +public class ExtractStringContribution extends RefactoringContribution { + + /* (non-Javadoc) + * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int) + */ + @SuppressWarnings("unchecked") + @Override + public RefactoringDescriptor createDescriptor( + String id, + String project, + String description, + String comment, + Map arguments, + int flags) + throws IllegalArgumentException { + return new ExtractStringDescriptor(project, description, comment, arguments); + } + + @SuppressWarnings("unchecked") + @Override + public Map retrieveArgumentMap(RefactoringDescriptor descriptor) { + if (descriptor instanceof ExtractStringDescriptor) { + return ((ExtractStringDescriptor) descriptor).getArguments(); + } + return super.retrieveArgumentMap(descriptor); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java new file mode 100644 index 0000000..190736a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java @@ -0,0 +1,71 @@ +/* + * 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.refactorings.extractstring; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; + +import java.util.Map; + +/** + * A descriptor that allows an {@link ExtractStringRefactoring} to be created from + * a previous instance of itself. + */ +public class ExtractStringDescriptor extends RefactoringDescriptor { + + public static final String ID = + "com.android.ide.eclipse.adt.refactoring.extract.string"; //$NON-NLS-1$ + + private final Map mArguments; + + public ExtractStringDescriptor(String project, String description, String comment, + Map arguments) { + super(ID, project, description, comment, + RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags + ); + mArguments = arguments; + } + + public Map getArguments() { + return mArguments; + } + + /** + * Creates a new refactoring instance for this refactoring descriptor based on + * an argument map. The argument map is created by the refactoring itself in + * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)} + *

          + * This is apparently used to replay a refactoring. + * + * {@inheritDoc} + * + * @throws CoreException + */ + @Override + public Refactoring createRefactoring(RefactoringStatus status) throws CoreException { + try { + ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments); + return ref; + } catch (NullPointerException e) { + status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor"); + return null; + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java new file mode 100644 index 0000000..de49d57 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java @@ -0,0 +1,505 @@ +/* + * 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.refactorings.extractstring; + + +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.ui.ConfigurationSelector; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.ltk.ui.refactoring.UserInputWizardPage; +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.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.HashMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @see ExtractStringRefactoring + */ +class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage { + + /** Last res file path used, shared across the session instances but specific to the + * current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */ + private static HashMap sLastResFilePath = new HashMap(); + + /** The project where the user selection happened. */ + private final IProject mProject; + + /** Test field where the user enters the new ID to be generated or replaced with. */ + private Text mStringIdField; + /** The configuration selector, to select the resource path of the XML file. */ + private ConfigurationSelector mConfigSelector; + /** The combo to display the existing XML files or enter a new one. */ + private Combo mResFileCombo; + + /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and + * a leaf file name ending with .xml */ + private static final Pattern RES_XML_FILE_REGEX = Pattern.compile( + "/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$ + /** Absolute destination folder root, e.g. "/res/" */ + private static final String RES_FOLDER_ABS = + AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP; + /** Relative destination folder root, e.g. "res/" */ + private static final String RES_FOLDER_REL = + SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; + + private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml"; //$NON-NLS-1$ + + private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); + + public ExtractStringInputPage(IProject project) { + super("ExtractStringInputPage"); //$NON-NLS-1$ + mProject = project; + } + + /** + * Create the UI for the refactoring wizard. + *

          + * Note that at that point the initial conditions have been checked in + * {@link ExtractStringRefactoring}. + */ + public void createControl(Composite parent) { + Composite content = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 1; + content.setLayout(layout); + + createStringGroup(content); + createResFileGroup(content); + + validatePage(); + setControl(content); + } + + /** + * Creates the top group with the field to replace which string and by what + * and by which options. + * + * @param content A composite with a 1-column grid layout + */ + public void createStringGroup(Composite content) { + + final ExtractStringRefactoring ref = getOurRefactoring(); + + Group group = new Group(content, SWT.NONE); + group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { + group.setText("String Replacement"); + } else { + group.setText("New String"); + } + + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + + // line: Textfield for string value (based on selection, if any) + + Label label = new Label(group, SWT.NONE); + label.setText("String"); + + String selectedString = ref.getTokenString(); + + final Text stringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + stringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + stringValueField.setText(selectedString != null ? selectedString : ""); //$NON-NLS-1$ + + ref.setNewStringValue(stringValueField.getText()); + + stringValueField.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (validatePage()) { + ref.setNewStringValue(stringValueField.getText()); + } + } + }); + + + // TODO provide an option to replace all occurences of this string instead of + // just the one. + + // line : Textfield for new ID + + label = new Label(group, SWT.NONE); + if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { + label.setText("Replace by R.string."); + } else if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { + label.setText("New R.string."); + } else { + label.setText("ID R.string."); + } + + mStringIdField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + mStringIdField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStringIdField.setText(guessId(selectedString)); + + ref.setNewStringId(mStringIdField.getText().trim()); + + mStringIdField.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + if (validatePage()) { + ref.setNewStringId(mStringIdField.getText().trim()); + } + } + }); + } + + /** + * Creates the lower group with the fields to choose the resource confirmation and + * the target XML file. + * + * @param content A composite with a 1-column grid layout + */ + private void createResFileGroup(Composite content) { + + Group group = new Group(content, SWT.NONE); + group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + group.setText("XML resource to edit"); + + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + + // line: selection of the res config + + Label label; + label = new Label(group, SWT.NONE); + label.setText("Configuration:"); + + mConfigSelector = new ConfigurationSelector(group); + GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); + gd.horizontalSpan = 2; + gd.widthHint = ConfigurationSelector.WIDTH_HINT; + gd.heightHint = ConfigurationSelector.HEIGHT_HINT; + mConfigSelector.setLayoutData(gd); + OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated(); + mConfigSelector.setOnChangeListener(onConfigSelectorUpdated); + + // line: selection of the output file + + label = new Label(group, SWT.NONE); + label.setText("Resource file:"); + + mResFileCombo = new Combo(group, SWT.DROP_DOWN); + mResFileCombo.select(0); + mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mResFileCombo.addModifyListener(onConfigSelectorUpdated); + + // set output file name to the last one used + + String projPath = mProject.getFullPath().toPortableString(); + String filePath = sLastResFilePath.get(projPath); + + mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH); + onConfigSelectorUpdated.run(); + } + + /** + * Utility method to guess a suitable new XML ID based on the selected string. + */ + private String guessId(String text) { + if (text == null) { + return ""; //$NON-NLS-1$ + } + + // make lower case + text = text.toLowerCase(); + + // everything not alphanumeric becomes an underscore + text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + + // the id must be a proper Java identifier, so it can't start with a number + if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) { + text = "_" + text; //$NON-NLS-1$ + } + return text; + } + + /** + * Returns the {@link ExtractStringRefactoring} instance used by this wizard page. + */ + private ExtractStringRefactoring getOurRefactoring() { + return (ExtractStringRefactoring) getRefactoring(); + } + + /** + * Validates fields of the wizard input page. Displays errors as appropriate and + * enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}. + * + * @return True if the page has been positively validated. It may still have warnings. + */ + private boolean validatePage() { + boolean success = true; + + // Analyze fatal errors. + + String text = mStringIdField.getText().trim(); + if (text == null || text.length() < 1) { + setErrorMessage("Please provide a resource ID."); + success = false; + } else { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + boolean ok = i == 0 ? + Character.isJavaIdentifierStart(c) : + Character.isJavaIdentifierPart(c); + if (!ok) { + setErrorMessage(String.format( + "The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.", + c, i+1)); + success = false; + break; + } + } + } + + String resFile = mResFileCombo.getText(); + if (success) { + if (resFile == null || resFile.length() == 0) { + setErrorMessage("A resource file name is required."); + success = false; + } else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) { + setErrorMessage("The XML file name is not valid."); + success = false; + } + } + + // Analyze info & warnings. + + if (success) { + setErrorMessage(null); + + ExtractStringRefactoring ref = getOurRefactoring(); + + ref.setTargetFile(resFile); + sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile); + + if (mXmlHelper.isResIdDuplicate(mProject, resFile, text)) { + String msg = String.format("There's already a string item called '%1$s' in %2$s.", + text, resFile); + if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { + setErrorMessage(msg); + success = false; + } else { + setMessage(msg, WizardPage.WARNING); + } + } else if (mProject.findMember(resFile) == null) { + setMessage( + String.format("File %2$s does not exist and will be created.", + text, resFile), + WizardPage.INFORMATION); + } else { + setMessage(null); + } + } + + setPageComplete(success); + return success; + } + + public class OnConfigSelectorUpdated implements Runnable, ModifyListener { + + /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */ + private final Pattern mPathRegex = Pattern.compile( + "(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$ + + /** Temporary config object used to retrieve the Config Selector value. */ + private FolderConfiguration mTempConfig = new FolderConfiguration(); + + private HashMap> mFolderCache = + new HashMap>(); + private String mLastFolderUsedInCombo = null; + private boolean mInternalConfigChange; + private boolean mInternalFileComboChange; + + /** + * Callback invoked when the {@link ConfigurationSelector} has been changed. + *

          + * The callback does the following: + *

            + *
          • Examine the current file name to retrieve the XML filename, if any. + *
          • Recompute the path based on the configuration selector (e.g. /res/values-fr/). + *
          • Examine the path to retrieve all the files in it. Keep those in a local cache. + *
          • If the XML filename from step 1 is not in the file list, it's a custom file name. + * Insert it and sort it. + *
          • Re-populate the file combo with all the choices. + *
          • Select the original XML file. + */ + public void run() { + if (mInternalConfigChange) { + return; + } + + // get current leafname, if any + String leafName = ""; //$NON-NLS-1$ + String currPath = mResFileCombo.getText(); + Matcher m = mPathRegex.matcher(currPath); + if (m.matches()) { + // Note: groups 1 and 2 cannot be null. + leafName = m.group(2); + currPath = m.group(1); + } else { + // There was a path but it was invalid. Ignore it. + currPath = ""; //$NON-NLS-1$ + } + + // recreate the res path from the current configuration + mConfigSelector.getConfiguration(mTempConfig); + StringBuffer sb = new StringBuffer(RES_FOLDER_ABS); + sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES)); + sb.append('/'); + + String newPath = sb.toString(); + if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) { + // Path has not changed. No need to reload. + return; + } + + // Get all the files at the new path + + TreeSet filePaths = mFolderCache.get(newPath); + + if (filePaths == null) { + filePaths = new TreeSet(); + + IFolder folder = mProject.getFolder(newPath); + if (folder != null && folder.exists()) { + try { + for (IResource res : folder.members()) { + String name = res.getName(); + if (res.getType() == IResource.FILE && name.endsWith(".xml")) { + filePaths.add(newPath + name); + } + } + } catch (CoreException e) { + // Ignore. + } + } + + mFolderCache.put(newPath, filePaths); + } + + currPath = newPath + leafName; + if (leafName.length() > 0 && !filePaths.contains(currPath)) { + filePaths.add(currPath); + } + + // Fill the combo + try { + mInternalFileComboChange = true; + + mResFileCombo.removeAll(); + + for (String filePath : filePaths) { + mResFileCombo.add(filePath); + } + + int index = -1; + if (leafName.length() > 0) { + index = mResFileCombo.indexOf(currPath); + if (index >= 0) { + mResFileCombo.select(index); + } + } + + if (index == -1) { + mResFileCombo.setText(currPath); + } + + mLastFolderUsedInCombo = newPath; + + } finally { + mInternalFileComboChange = false; + } + + // finally validate the whole page + validatePage(); + } + + /** + * Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been + * modified. + */ + public void modifyText(ModifyEvent e) { + if (mInternalFileComboChange) { + return; + } + + String wsFolderPath = mResFileCombo.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()); + + mInternalFileComboChange = true; + mResFileCombo.setText(wsFolderPath); + mInternalFileComboChange = false; + } + + if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { + wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); + + int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR); + if (pos >= 0) { + wsFolderPath = wsFolderPath.substring(0, pos); + } + + String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP); + + if (folderSegments.length > 0) { + String folderName = folderSegments[0]; + + if (folderName != null && !folderName.equals(wsFolderPath)) { + // update config selector + mInternalConfigChange = true; + mConfigSelector.setConfiguration(folderSegments); + mInternalConfigChange = false; + } + } + } + + validatePage(); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java new file mode 100644 index 0000000..63184ad --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java @@ -0,0 +1,988 @@ +/* + * 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.refactorings.extractstring; + +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestParser; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourceAttributes; +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.Path; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.ToolFactory; +import org.eclipse.jdt.core.compiler.IScanner; +import org.eclipse.jdt.core.compiler.ITerminalSymbols; +import org.eclipse.jdt.core.compiler.InvalidInputException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.ChangeDescriptor; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; +import org.eclipse.ltk.core.refactoring.TextFileChange; +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.eclipse.text.edits.TextEditGroup; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This refactoring extracts a string from a file and replaces it by an Android resource ID + * such as R.string.foo. + *

            + * There are a number of scenarios, which are not all supported yet. The workflow works as + * such: + *

              + *
            • User selects a string in a Java (TODO: or XML file) and invokes + * the {@link ExtractStringAction}. + *
            • The action finds the {@link ICompilationUnit} being edited as well as the current + * {@link ITextSelection}. The action creates a new instance of this refactoring as + * well as an {@link ExtractStringWizard} and runs the operation. + *
            • TODO: to support refactoring from an XML file, the action should give the {@link IFile} + * and then here we would have to determine whether it's a suitable Android XML file or a + * suitable Java file. + * TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout + * files or text elements (e.g. foo) for values, etc. + *
            • Step 1 of the refactoring is to check the preliminary conditions. Right now we check + * that the java source is not read-only and is in sync. We also try to find a string under + * the selection. If this fails, the refactoring is aborted. + *
            • TODO: Find the string in an XML file based on selection. + *
            • On success, the wizard is shown, which let the user input the new ID to use. + *
            • The wizard sets the user input values into this refactoring instance, e.g. the new string + * ID, the XML file to update, etc. The wizard does use the utility method + * {@link XmlStringFileHelper#isResIdDuplicate(IProject, String, String)} to check whether + * the new ID is already defined in the target XML file. + *
            • Once Preview or Finish is selected in the wizard, the + * {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input + * and compute the actual changes. + *
            • When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked. + *
            + * + * The list of changes are: + *
              + *
            • If the target XML does not exist, create it with the new string ID. + *
            • If the target XML exists, find the node and add the new string ID right after. + * If the node is , it needs to be opened. + *
            • Create an AST rewriter to edit the source Java file and replace all occurences by the + * new computed R.string.foo. Also need to rewrite imports to import R as needed. + * If there's already a conflicting R included, we need to insert the FQCN instead. + *
            • TODO: If the source is an XML file, determine if we need to change an attribute or a + * a text element. + *
            • TODO: Have a pref in the wizard: [x] Change other XML Files + *
            • TODO: Have a pref in the wizard: [x] Change other Java Files + *
            + */ +public class ExtractStringRefactoring extends Refactoring { + + public enum Mode { + /** + * the Extract String refactoring is called on an existing source file. + * Its purpose is then to get the selected string of the source and propose to + * change it by an XML id. The XML id may be a new one or an existing one. + */ + EDIT_SOURCE, + /** + * The Extract String refactoring is called without any source file. + * Its purpose is then to create a new XML string ID or select/modify an existing one. + */ + SELECT_ID, + /** + * The Extract String refactoring is called without any source file. + * Its purpose is then to create a new XML string ID. The ID must not already exist. + */ + SELECT_NEW_ID + } + + /** The {@link Mode} of operation of the refactoring. */ + private final Mode mMode; + /** The file model being manipulated. + * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ + private final IFile mFile; + /** The project that contains {@link #mFile} and that contains the target XML file to modify. */ + private final IProject mProject; + /** The start of the selection in {@link #mFile}. + * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ + private final int mSelectionStart; + /** The end of the selection in {@link #mFile}. + * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ + private final int mSelectionEnd; + + /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */ + private ICompilationUnit mUnit; + /** The actual string selected, after UTF characters have been escaped, good for display. + * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ + private String mTokenString; + + /** The XML string ID selected by the user in the wizard. */ + private String mXmlStringId; + /** The XML string value. Might be different than the initial selected string. */ + private String mXmlStringValue; + /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user + * in the wizard. */ + private String mTargetXmlFileWsPath; + + /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and + * used by {@link #createChange(IProgressMonitor)}. */ + private ArrayList mChanges; + + private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); + + private static final String KEY_MODE = "mode"; //$NON-NLS-1$ + private static final String KEY_FILE = "file"; //$NON-NLS-1$ + private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$ + private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$ + private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$ + private static final String KEY_TOK_ESC = "tok-esc"; //$NON-NLS-1$ + + public ExtractStringRefactoring(Map arguments) + throws NullPointerException { + mMode = Mode.valueOf(arguments.get(KEY_MODE)); + + IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT)); + mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path); + + if (mMode == Mode.EDIT_SOURCE) { + path = Path.fromPortableString(arguments.get(KEY_FILE)); + mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path); + + mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START)); + mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END)); + mTokenString = arguments.get(KEY_TOK_ESC); + } else { + mFile = null; + mSelectionStart = mSelectionEnd = -1; + mTokenString = null; + } + } + + private Map createArgumentMap() { + HashMap args = new HashMap(); + args.put(KEY_MODE, mMode.name()); + args.put(KEY_PROJECT, mProject.getFullPath().toPortableString()); + if (mMode == Mode.EDIT_SOURCE) { + args.put(KEY_FILE, mFile.getFullPath().toPortableString()); + args.put(KEY_SEL_START, Integer.toString(mSelectionStart)); + args.put(KEY_SEL_END, Integer.toString(mSelectionEnd)); + args.put(KEY_TOK_ESC, mTokenString); + } + return args; + } + + /** + * Constructor to use when the Extract String refactoring is called on an + * *existing* source file. Its purpose is then to get the selected string of + * the source and propose to change it by an XML id. The XML id may be a new one + * or an existing one. + * + * @param file The source file to process. Cannot be null. File must exist in workspace. + * @param selection The selection in the source file. Cannot be null or empty. + */ + public ExtractStringRefactoring(IFile file, ITextSelection selection) { + mMode = Mode.EDIT_SOURCE; + mFile = file; + mProject = file.getProject(); + mSelectionStart = selection.getOffset(); + mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1); + } + + /** + * Constructor to use when the Extract String refactoring is called without + * any source file. Its purpose is then to create a new XML string ID. + * + * @param project The project where the target XML file to modify is located. Cannot be null. + * @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be + * used. + */ + public ExtractStringRefactoring(IProject project, boolean enforceNew) { + mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID; + mFile = null; + mProject = project; + mSelectionStart = mSelectionEnd = -1; + } + + /** + * @see org.eclipse.ltk.core.refactoring.Refactoring#getName() + */ + @Override + public String getName() { + if (mMode == Mode.SELECT_ID) { + return "Create or USe Android String"; + } else if (mMode == Mode.SELECT_NEW_ID) { + return "Create New Android String"; + } + + return "Extract Android String"; + } + + public Mode getMode() { + return mMode; + } + + /** + * Gets the actual string selected, after UTF characters have been escaped, + * good for display. + */ + public String getTokenString() { + return mTokenString; + } + + public String getXmlStringId() { + return mXmlStringId; + } + + /** + * Step 1 of 3 of the refactoring: + * Checks that the current selection meets the initial condition before the ExtractString + * wizard is shown. The check is supposed to be lightweight and quick. Note that at that + * point the wizard has not been created yet. + *

            + * Here we scan the source buffer to find the token matching the selection. + * The check is successful is a Java string literal is selected, the source is in sync + * and is not read-only. + *

            + * This is also used to extract the string to be modified, so that we can display it in + * the refactoring wizard. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + + mUnit = null; + mTokenString = null; + + RefactoringStatus status = new RefactoringStatus(); + + try { + monitor.beginTask("Checking preconditions...", 5); + + if (mMode != Mode.EDIT_SOURCE) { + monitor.worked(5); + return status; + } + + if (!checkSourceFile(mFile, status, monitor)) { + return status; + } + + // Try to get a compilation unit from this file. If it fails, mUnit is null. + try { + mUnit = JavaCore.createCompilationUnitFrom(mFile); + + // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar + if (mUnit.isReadOnly()) { + status.addFatalError("The file is read-only, please make it writeable first."); + return status; + } + + // This is a Java file. Check if it contains the selection we want. + if (!findSelectionInJavaUnit(mUnit, status, monitor)) { + return status; + } + + } catch (Exception e) { + // That was not a Java file. Ignore. + } + + if (mUnit == null) { + // Check this an XML file and get the selection and its context. + // TODO + status.addFatalError("Selection must be inside a Java source file."); + } + } finally { + monitor.done(); + } + + return status; + } + + /** + * Try to find the selected Java element in the compilation unit. + * + * If selection matches a string literal, capture it, otherwise add a fatal error + * to the status. + * + * On success, advance the monitor by 3. + */ + private boolean findSelectionInJavaUnit(ICompilationUnit unit, + RefactoringStatus status, IProgressMonitor monitor) { + try { + IBuffer buffer = unit.getBuffer(); + + IScanner scanner = ToolFactory.createScanner( + false, //tokenizeComments + false, //tokenizeWhiteSpace + false, //assertMode + false //recordLineSeparator + ); + scanner.setSource(buffer.getCharacters()); + monitor.worked(1); + + for(int token = scanner.getNextToken(); + token != ITerminalSymbols.TokenNameEOF; + token = scanner.getNextToken()) { + if (scanner.getCurrentTokenStartPosition() <= mSelectionStart && + scanner.getCurrentTokenEndPosition() >= mSelectionEnd) { + // found the token, but only keep of the right type + if (token == ITerminalSymbols.TokenNameStringLiteral) { + mTokenString = new String(scanner.getCurrentTokenSource()); + } + break; + } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) { + // scanner is past the selection, abort. + break; + } + } + } catch (JavaModelException e1) { + // Error in unit.getBuffer. Ignore. + } catch (InvalidInputException e2) { + // Error in scanner.getNextToken. Ignore. + } finally { + monitor.worked(1); + } + + if (mTokenString != null) { + // As a literal string, the token should have surrounding quotes. Remove them. + int len = mTokenString.length(); + if (len > 0 && + mTokenString.charAt(0) == '"' && + mTokenString.charAt(len - 1) == '"') { + mTokenString = mTokenString.substring(1, len - 1); + } + // We need a non-empty string literal + if (mTokenString.length() == 0) { + mTokenString = null; + } + } + + if (mTokenString == null) { + status.addFatalError("Please select a Java string literal."); + } + + monitor.worked(1); + return status.isOK(); + } + + /** + * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit() + * Might not be useful. + * + * On success, advance the monitor by 2. + * + * @return False if caller should abort, true if caller should continue. + */ + private boolean checkSourceFile(IFile file, + RefactoringStatus status, + IProgressMonitor monitor) { + // check whether the source file is in sync + if (!file.isSynchronized(IResource.DEPTH_ZERO)) { + status.addFatalError("The file is not synchronized. Please save it first."); + return false; + } + monitor.worked(1); + + // make sure we can write to it. + ResourceAttributes resAttr = file.getResourceAttributes(); + if (resAttr == null || resAttr.isReadOnly()) { + status.addFatalError("The file is read-only, please make it writeable first."); + return false; + } + monitor.worked(1); + + return true; + } + + /** + * Step 2 of 3 of the refactoring: + * Check the conditions once the user filled values in the refactoring wizard, + * then prepare the changes to be applied. + *

            + * In this case, most of the sanity checks are done by the wizard so essentially this + * should only be called if the wizard positively validated the user input. + * + * Here we do check that the target resource XML file either does not exists or + * is not read-only. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + RefactoringStatus status = new RefactoringStatus(); + + try { + monitor.beginTask("Checking post-conditions...", 3); + + if (mXmlStringId == null || mXmlStringId.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing replacement string ID"); + } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing target xml file path"); + } + monitor.worked(1); + + // Either that resource must not exist or it must be a writeable file. + IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath); + if (targetXml != null) { + if (targetXml.getType() != IResource.FILE) { + status.addFatalError( + String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath)); + } else { + ResourceAttributes attr = targetXml.getResourceAttributes(); + if (attr != null && attr.isReadOnly()) { + status.addFatalError( + String.format("XML file '%1$s' is read-only.", + mTargetXmlFileWsPath)); + } + } + } + monitor.worked(1); + + if (status.hasError()) { + return status; + } + + mChanges = new ArrayList(); + + + // Prepare the change for the XML file. + + if (!mXmlHelper.isResIdDuplicate(mProject, mTargetXmlFileWsPath, mXmlStringId)) { + // We actually change it only if the ID doesn't exist yet + Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue, + status, SubMonitor.convert(monitor, 1)); + if (change != null) { + mChanges.add(change); + } + } + + if (status.hasError()) { + return status; + } + + if (mMode == Mode.EDIT_SOURCE) { + // Prepare the change to the Java compilation unit + List changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString, + status, SubMonitor.convert(monitor, 1)); + if (changes != null) { + mChanges.addAll(changes); + } + } + + monitor.worked(1); + } finally { + monitor.done(); + } + + return status; + } + + /** + * Internal helper that actually prepares the {@link Change} that adds the given + * ID to the given XML File. + *

            + * This does not actually modify the file. + * + * @param targetXml The file resource to modify. + * @param xmlStringId The new ID to insert. + * @param tokenString The old string, which will be the value in the XML string. + * @return A new {@link TextEdit} that describes how to change the file. + */ + private Change createXmlChange(IFile targetXml, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor subMonitor) { + + TextFileChange xmlChange = new TextFileChange(getName(), targetXml); + xmlChange.setTextType("xml"); //$NON-NLS-1$ + + TextEdit edit = null; + TextEditGroup editGroup = null; + + if (!targetXml.exists()) { + // The XML file does not exist. Simply create it. + StringBuilder content = new StringBuilder(); + content.append("\n"); //$NON-NLS-1$ + content.append("\n"); //$NON-NLS-1$ + content.append(" "). //$NON-NLS-1$ + append(tokenString). + append("\n"); //$NON-NLS-1$ + content.append("\n"); //$NON-NLS-1$ + + edit = new InsertEdit(0, content.toString()); + editGroup = new TextEditGroup("Create in new XML file", edit); + } else { + // The file exist. Attempt to parse it as a valid XML document. + try { + int[] indices = new int[2]; + + // TODO case where we replace the value of an existing XML String ID + + if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$ + // Indices[1] indicates whether we found > or />. It can only be 1 or 2. + // Indices[0] is the position of the first character of either > or />. + // + // Note: we don't even try to adapt our formatting to the existing structure (we + // could by capturing whatever whitespace is after the closing bracket and + // applying it here before our tag, unless we were dealing with an empty + // resource tag.) + + int offset = indices[0]; + int len = indices[1]; + StringBuilder content = new StringBuilder(); + content.append(">\n"); //$NON-NLS-1$ + content.append(" "). //$NON-NLS-1$ + append(tokenString). + append(""); //$NON-NLS-1$ + if (len == 2) { + content.append("\n"); //$NON-NLS-1$ + } + + edit = new ReplaceEdit(offset, len, content.toString()); + editGroup = new TextEditGroup("Insert in XML file", edit); + } + } catch (CoreException e) { + // Failed to read file. Ignore. Will return null below. + } + } + + if (edit == null) { + status.addFatalError(String.format("Failed to modify file %1$s", + mTargetXmlFileWsPath)); + return null; + } + + xmlChange.setEdit(edit); + // The TextEditChangeGroup let the user toggle this change on and off later. + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup)); + + subMonitor.worked(1); + return xmlChange; + } + + /** + * Parse an XML input stream, looking for an opening tag. + *

            + * If found, returns the character offest in the buffer of the closing bracket of that + * tag, e.g. the position of > in "". The first character is at offset 0. + *

            + * The implementation here relies on a simple character-based parser. No DOM nor SAX + * parsing is used, due to the simplified nature of the task: we just want the first + * opening tag, which in our case should be the document root. We deal however with + * with the tag being commented out, so comments are skipped. We assume the XML doc + * is sane, e.g. we don't expect the tag to appear in the middle of a string. But + * again since in fact we want the root element, that's unlikely to happen. + *

            + * We need to deal with the case where the element is written as , in + * which case the caller will want to replace /> by ">...". To do that we return + * two values: the first offset of the closing tag (e.g. / or >) and the length, which + * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >. + * + * @param contents An existing buffer to parse. + * @param tag The tag to look for. + * @param indices The return values: [0] is the offset of the closing bracket and [1] is + * the length which can be only 1 for > and 2 for /> + * @return True if we found the tag, in which case indices can be used. + */ + private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) { + + BufferedReader br = new BufferedReader(new InputStreamReader(contents)); + StringBuilder sb = new StringBuilder(); // scratch area + + tag = "<" + tag; + int tagLen = tag.length(); + int maxLen = tagLen < 3 ? 3 : tagLen; + + try { + int offset = 0; + int i = 0; + char searching = '<'; // we want opening tags + boolean capture = false; + boolean inComment = false; + boolean inTag = false; + while ((i = br.read()) != -1) { + char c = (char) i; + if (c == searching) { + capture = true; + } + if (capture) { + sb.append(c); + int len = sb.length(); + if (inComment && c == '>') { + // is the comment being closed? + if (len >= 3 && sb.substring(len-3).equals("-->")) { //$NON-NLS-1$ + // yes, comment is closing, stop capturing + capture = false; + inComment = false; + sb.setLength(0); + } + } else if (inTag && c == '>') { + // we're capturing in our tag, waiting for the closing >, we just got it + // so we're totally done here. Simply detect whether it's /> or >. + indices[0] = offset; + indices[1] = 1; + if (sb.charAt(len - 2) == '/') { + indices[0]--; + indices[1]++; + } + return true; + + } else if (!inComment && !inTag) { + // not a comment and not our tag yet, so we're capturing because a + // tag is being opened but we don't know which one yet. + + // look for either the opening or a comment or + // the opening of our tag. + if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$ + inComment = true; + } else if (len == tagLen && sb.toString().equals(tag)) { + inTag = true; + } + + // if we're not interested in this tag yet, deal with when to stop + // capturing: the opening tag ends with either any kind of whitespace + // or with a > or maybe there's a PI that starts with ' || c == '?' || c == ' ' || c == '\n' || c == '\r') { + // stop capturing + capture = false; + sb.setLength(0); + } + } + } + + if (capture && len > maxLen) { + // in any case we don't need to capture more than the size of our tag + // or the comment opening tag + sb.deleteCharAt(0); + } + } + offset++; + } + } catch (IOException e) { + // Ignore. + } finally { + try { + br.close(); + } catch (IOException e) { + // oh come on... + } + } + + return false; + } + + /** + * Computes the changes to be made to Java file(s) and returns a list of {@link Change}. + */ + private List computeJavaChanges(ICompilationUnit unit, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor subMonitor) { + + // Get the Android package name from the Android Manifest. We need it to create + // the FQCN of the R class. + String packageName = null; + String error = null; + IResource manifestFile = mProject.findMember(AndroidConstants.FN_ANDROID_MANIFEST); + if (manifestFile == null || manifestFile.getType() != IResource.FILE) { + error = "File not found"; + } else { + try { + AndroidManifestParser manifest = AndroidManifestParser.parseForData( + (IFile) manifestFile); + if (manifest == null) { + error = "Invalid content"; + } else { + packageName = manifest.getPackage(); + if (packageName == null) { + error = "Missing package definition"; + } + } + } catch (CoreException e) { + error = e.getLocalizedMessage(); + } + } + + if (error != null) { + status.addFatalError( + String.format("Failed to parse file %1$s: %2$s.", + manifestFile.getFullPath(), error)); + return null; + } + + // TODO in a future version we might want to collect various Java files that + // need to be updated in the same project and process them all together. + // To do that we need to use an ASTRequestor and parser.createASTs, kind of + // like this: + // + // ASTRequestor requestor = new ASTRequestor() { + // @Override + // public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) { + // super.acceptAST(sourceUnit, astNode); + // // TODO process astNode + // } + // }; + // ... + // parser.createASTs(compilationUnits, bindingKeys, requestor, monitor) + // + // and then add multiple TextFileChange to the changes arraylist. + + // Right now the changes array will contain one TextFileChange at most. + ArrayList changes = new ArrayList(); + + // This is the unit that will be modified. + TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource()); + change.setTextType("java"); //$NON-NLS-1$ + + // Create an AST for this compilation unit + ASTParser parser = ASTParser.newParser(AST.JLS3); + parser.setProject(unit.getJavaProject()); + parser.setSource(unit); + parser.setResolveBindings(true); + ASTNode node = parser.createAST(subMonitor.newChild(1)); + + // The ASTNode must be a CompilationUnit, by design + if (!(node instanceof CompilationUnit)) { + status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$ + node.getClass())); + return null; + } + + // ImportRewrite will allow us to add the new type to the imports and will resolve + // what the Java source must reference, e.g. the FQCN or just the simple name. + ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true); + String Rqualifier = packageName + ".R"; //$NON-NLS-1$ + Rqualifier = importRewrite.addImport(Rqualifier); + + // Rewrite the AST itself via an ASTVisitor + AST ast = node.getAST(); + ASTRewrite astRewrite = ASTRewrite.create(ast); + ArrayList astEditGroups = new ArrayList(); + ReplaceStringsVisitor visitor = new ReplaceStringsVisitor( + ast, astRewrite, astEditGroups, + tokenString, Rqualifier, xmlStringId); + node.accept(visitor); + + // Finally prepare the change set + try { + MultiTextEdit edit = new MultiTextEdit(); + + // Create the edit to change the imports, only if anything changed + TextEdit subEdit = importRewrite.rewriteImports(subMonitor.newChild(1)); + if (subEdit.hasChildren()) { + edit.addChild(subEdit); + } + + // Create the edit to change the Java source, only if anything changed + subEdit = astRewrite.rewriteAST(); + if (subEdit.hasChildren()) { + edit.addChild(subEdit); + } + + // Only create a change set if any edit was collected + if (edit.hasChildren()) { + change.setEdit(edit); + + // Create TextEditChangeGroups which let the user turn changes on or off + // individually. This must be done after the change.setEdit() call above. + for (TextEditGroup editGroup : astEditGroups) { + change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup)); + } + + changes.add(change); + } + + // TODO to modify another Java source, loop back to the creation of the + // TextFileChange and accumulate in changes. Right now only one source is + // modified. + + if (changes.size() > 0) { + return changes; + } + + subMonitor.worked(1); + + } catch (CoreException e) { + // ImportRewrite.rewriteImports failed. + status.addFatalError(e.getMessage()); + } + return null; + } + + public class ReplaceStringsVisitor extends ASTVisitor { + + private final AST mAst; + private final ASTRewrite mRewriter; + private final String mOldString; + private final String mRQualifier; + private final String mXmlId; + private final ArrayList mEditGroups; + + public ReplaceStringsVisitor(AST ast, + ASTRewrite astRewrite, + ArrayList editGroups, + String oldString, + String rQualifier, + String xmlId) { + mAst = ast; + mRewriter = astRewrite; + mEditGroups = editGroups; + mOldString = oldString; + mRQualifier = rQualifier; + mXmlId = xmlId; + } + + @Override + public boolean visit(StringLiteral node) { + if (node.getLiteralValue().equals(mOldString)) { + + Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ + SimpleName idName = mAst.newSimpleName(mXmlId); + QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName); + + TextEditGroup editGroup = new TextEditGroup("Replace string by ID"); + mEditGroups.add(editGroup); + mRewriter.replace(node, newNode, editGroup); + } + return super.visit(node); + } + } + + /** + * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the + * work and creates a descriptor that can be used to replay that refactoring later. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor) + * + * @throws CoreException + */ + @Override + public Change createChange(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + + try { + monitor.beginTask("Applying changes...", 1); + + CompositeChange change = new CompositeChange( + getName(), + mChanges.toArray(new Change[mChanges.size()])) { + @Override + public ChangeDescriptor getDescriptor() { + + String comment = String.format( + "Extracts string '%1$s' into R.string.%2$s", + mTokenString, + mXmlStringId); + + ExtractStringDescriptor desc = new ExtractStringDescriptor( + mProject.getName(), //project + comment, //description + comment, //comment + createArgumentMap()); + + return new RefactoringChangeDescriptor(desc); + } + }; + + monitor.worked(1); + + return change; + + } finally { + monitor.done(); + } + + } + + /** + * Given a file project path, returns its resource in the same project than the + * compilation unit. The resource may not exist. + */ + private IResource getTargetXmlResource(String xmlFileWsPath) { + IResource resource = mProject.getFile(xmlFileWsPath); + return resource; + } + + /** + * Sets the replacement string ID. Used by the wizard to set the user input. + */ + public void setNewStringId(String newStringId) { + mXmlStringId = newStringId; + } + + /** + * Sets the replacement string ID. Used by the wizard to set the user input. + */ + public void setNewStringValue(String newStringValue) { + mXmlStringValue = newStringValue; + } + + /** + * Sets the target file. This is a project path, e.g. "/res/values/strings.xml". + * Used by the wizard to set the user input. + */ + public void setTargetFile(String targetXmlFileWsPath) { + mTargetXmlFileWsPath = targetXmlFileWsPath; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java new file mode 100644 index 0000000..556dff0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java @@ -0,0 +1,50 @@ +/* + * 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.refactorings.extractstring; + +import org.eclipse.core.resources.IProject; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; + +/** + * A wizard for ExtractString based on a simple dialog with one page. + * + * @see ExtractStringInputPage + * @see ExtractStringRefactoring + */ +public class ExtractStringWizard extends RefactoringWizard { + + private final IProject mProject; + + /** + * Create a wizard for ExtractString based on a simple dialog with one page. + * + * @param ref The instance of {@link ExtractStringRefactoring} to associate to the wizard. + * @param project The project where the wizard was invoked from (e.g. where the user selection + * happened, so that we can retrieve project resources.) + */ + public ExtractStringWizard(ExtractStringRefactoring ref, IProject project) { + super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE); + mProject = project; + setDefaultPageTitle(ref.getName()); + } + + @Override + protected void addUserInputPages() { + addPage(new ExtractStringInputPage(mProject)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java new file mode 100644 index 0000000..0c9389f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java @@ -0,0 +1,122 @@ +/* + * 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.refactorings.extractstring; + +import com.android.ide.eclipse.common.project.AndroidXPathFactory; + +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.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import java.util.HashMap; +import java.util.HashSet; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +/** + * + */ +class XmlStringFileHelper { + + /** A temporary cache of R.string IDs defined by a given xml file. The key is the + * project path of the file, the data is a set of known string Ids for that file. */ + private HashMap> mResIdCache; + /** An instance of XPath, created lazily on demand. */ + private XPath mXPath; + + public XmlStringFileHelper() { + } + + /** + * Utility method used by the wizard to check whether the given string ID is already + * defined in the XML file which path is given. + * + * @param project The project contain the XML file. + * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml". + * The given file may or may not exist. + * @param stringId The string ID to find. + * @return True if such a string ID is already defined. + */ + public boolean isResIdDuplicate(IProject project, String xmlFileWsPath, String stringId) { + // This is going to be called many times on the same file. + // Build a cache of the existing IDs for a given file. + if (mResIdCache == null) { + mResIdCache = new HashMap>(); + } + HashSet cache = mResIdCache.get(xmlFileWsPath); + if (cache == null) { + cache = getResIdsForFile(project, xmlFileWsPath); + mResIdCache.put(xmlFileWsPath, cache); + } + + return cache.contains(stringId); + } + + /** + * Extract all the defined string IDs from a given file using XPath. + * @param project The project contain the XML file. + * @param xmlFileWsPath The project path of the file to parse. It may not exist. + * @return The set of all string IDs defined in the file. The returned set is always non + * null. It is empty if the file does not exist. + */ + private HashSet getResIdsForFile(IProject project, String xmlFileWsPath) { + HashSet ids = new HashSet(); + + if (mXPath == null) { + mXPath = AndroidXPathFactory.newXPath(); + } + + // Access the project that contains the resource that contains the compilation unit + IResource resource = project.getFile(xmlFileWsPath); + + if (resource != null && resource.exists() && resource.getType() == IResource.FILE) { + InputSource source; + try { + source = new InputSource(((IFile) resource).getContents()); + + // We want all the IDs in an XML structure like this: + // + // something + // + + String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$ + + Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET); + if (result instanceof NodeList) { + NodeList list = (NodeList) result; + for (int n = list.getLength() - 1; n >= 0; n--) { + String id = list.item(n).getNodeValue(); + ids.add(id); + } + } + + } catch (CoreException e1) { + // IFile.getContents failed. Ignore. + } catch (XPathExpressionException e) { + // mXPath.evaluate failed. Ignore. + } + } + + return ids; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java deleted file mode 100644 index eb8a4b8..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.internal.build.ApkBuilder; -import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder; -import com.android.ide.eclipse.adt.internal.build.ResourceManagerBuilder; - -import org.eclipse.core.resources.ICommand; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IProjectDescription; -import org.eclipse.core.resources.IProjectNature; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.SubProgressMonitor; -import org.eclipse.jdt.core.JavaCore; - -/** - * Project nature for the Android Projects. - */ -public class AndroidNature implements IProjectNature { - - /** the project this nature object is associated with */ - private IProject mProject; - - /** - * Configures this nature for its project. This is called by the workspace - * when natures are added to the project using - * IProject.setDescription and should not be called directly - * by clients. The nature extension id is added to the list of natures - * before this method is called, and need not be added here. - * - * Exceptions thrown by this method will be propagated back to the caller of - * IProject.setDescription, but the nature will remain in - * the project description. - * - * The Android nature adds the pre-builder and the APK builder if necessary. - * - * @see org.eclipse.core.resources.IProjectNature#configure() - * @throws CoreException if configuration fails. - */ - public void configure() throws CoreException { - configureResourceManagerBuilder(mProject); - configurePreBuilder(mProject); - configureApkBuilder(mProject); - } - - /** - * De-configures this nature for its project. This is called by the - * workspace when natures are removed from the project using - * IProject.setDescription and should not be called directly - * by clients. The nature extension id is removed from the list of natures - * before this method is called, and need not be removed here. - * - * Exceptions thrown by this method will be propagated back to the caller of - * IProject.setDescription, but the nature will still be - * removed from the project description. - * - * The Android nature removes the custom pre builder and APK builder. - * - * @see org.eclipse.core.resources.IProjectNature#deconfigure() - * @throws CoreException if configuration fails. - */ - public void deconfigure() throws CoreException { - // remove the android builders - removeBuilder(mProject, ResourceManagerBuilder.ID); - removeBuilder(mProject, PreCompilerBuilder.ID); - removeBuilder(mProject, ApkBuilder.ID); - } - - /** - * Returns the project to which this project nature applies. - * - * @return the project handle - * @see org.eclipse.core.resources.IProjectNature#getProject() - */ - public IProject getProject() { - return mProject; - } - - /** - * Sets the project to which this nature applies. Used when instantiating - * this project nature runtime. This is called by - * IProject.create() or - * IProject.setDescription() and should not be called - * directly by clients. - * - * @param project the project to which this nature applies - * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject) - */ - public void setProject(IProject project) { - mProject = project; - } - - /** - * Adds the Android Nature and the Java Nature to the project if it doesn't - * already have them. - * - * @param project An existing or new project to update - * @param monitor An optional progress monitor. Can be null. - * @throws CoreException if fails to change the nature. - */ - public static synchronized void setupProjectNatures(IProject project, - IProgressMonitor monitor) throws CoreException { - if (project == null || !project.isOpen()) return; - if (monitor == null) monitor = new NullProgressMonitor(); - - // Add the natures. We need to add the Java nature first, so it adds its builder to the - // project first. This way, when the android nature is added, we can control where to put - // the android builders in relation to the java builder. - // Adding the java nature after the android one, would place the java builder before the - // android builders. - addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor); - addNatureToProjectDescription(project, AndroidConstants.NATURE, monitor); - } - - /** - * Add the specified nature to the specified project. The nature is only - * added if not already present. - *

            - * Android Natures are always inserted at the beginning of the list of natures in order to - * have the jdt views/dialogs display the proper icon. - * - * @param project The project to modify. - * @param natureId The Id of the nature to add. - * @param monitor An existing progress monitor. - * @throws CoreException if fails to change the nature. - */ - private static void addNatureToProjectDescription(IProject project, - String natureId, IProgressMonitor monitor) throws CoreException { - if (!project.hasNature(natureId)) { - - IProjectDescription description = project.getDescription(); - String[] natures = description.getNatureIds(); - String[] newNatures = new String[natures.length + 1]; - - // Android natures always come first. - if (natureId.equals(AndroidConstants.NATURE)) { - System.arraycopy(natures, 0, newNatures, 1, natures.length); - newNatures[0] = natureId; - } else { - System.arraycopy(natures, 0, newNatures, 0, natures.length); - newNatures[natures.length] = natureId; - } - - description.setNatureIds(newNatures); - project.setDescription(description, new SubProgressMonitor(monitor, 10)); - } - } - - /** - * Adds the ResourceManagerBuilder, if its not already there. It'll insert - * itself as the first builder. - * @throws CoreException - * - */ - public static void configureResourceManagerBuilder(IProject project) - throws CoreException { - // get the builder list - IProjectDescription desc = project.getDescription(); - ICommand[] commands = desc.getBuildSpec(); - - // look for the builder in case it's already there. - for (int i = 0; i < commands.length; ++i) { - if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { - return; - } - } - - // it's not there, lets add it at the beginning of the builders - ICommand[] newCommands = new ICommand[commands.length + 1]; - System.arraycopy(commands, 0, newCommands, 1, commands.length); - ICommand command = desc.newCommand(); - command.setBuilderName(ResourceManagerBuilder.ID); - newCommands[0] = command; - desc.setBuildSpec(newCommands); - project.setDescription(desc, null); - } - - /** - * Adds the PreCompilerBuilder if its not already there. It'll check for - * presence of the ResourceManager and insert itself right after. - * @param project - * @throws CoreException - */ - public static void configurePreBuilder(IProject project) - throws CoreException { - // get the builder list - IProjectDescription desc = project.getDescription(); - ICommand[] commands = desc.getBuildSpec(); - - // look for the builder in case it's already there. - for (int i = 0; i < commands.length; ++i) { - if (PreCompilerBuilder.ID.equals(commands[i].getBuilderName())) { - return; - } - } - - // we need to add it after the resource manager builder. - // Let's look for it - int index = -1; - for (int i = 0; i < commands.length; ++i) { - if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { - index = i; - break; - } - } - - // we're inserting after - index++; - - // do the insertion - - // copy the builders before. - ICommand[] newCommands = new ICommand[commands.length + 1]; - System.arraycopy(commands, 0, newCommands, 0, index); - - // insert the new builder - ICommand command = desc.newCommand(); - command.setBuilderName(PreCompilerBuilder.ID); - newCommands[index] = command; - - // copy the builder after - System.arraycopy(commands, index, newCommands, index + 1, commands.length-index); - - // set the new builders in the project - desc.setBuildSpec(newCommands); - project.setDescription(desc, null); - } - - public static void configureApkBuilder(IProject project) - throws CoreException { - // Add the .apk builder at the end if it's not already there - IProjectDescription desc = project.getDescription(); - ICommand[] commands = desc.getBuildSpec(); - - for (int i = 0; i < commands.length; ++i) { - if (ApkBuilder.ID.equals(commands[i].getBuilderName())) { - return; - } - } - - ICommand[] newCommands = new ICommand[commands.length + 1]; - System.arraycopy(commands, 0, newCommands, 0, commands.length); - ICommand command = desc.newCommand(); - command.setBuilderName(ApkBuilder.ID); - newCommands[commands.length] = command; - desc.setBuildSpec(newCommands); - project.setDescription(desc, null); - } - - /** - * Removes a builder from the project. - * @param project The project to remove the builder from. - * @param id The String ID of the builder to remove. - * @return true if the builder was found and removed. - * @throws CoreException - */ - public static boolean removeBuilder(IProject project, String id) throws CoreException { - IProjectDescription description = project.getDescription(); - ICommand[] commands = description.getBuildSpec(); - for (int i = 0; i < commands.length; ++i) { - if (id.equals(commands[i].getBuilderName())) { - ICommand[] newCommands = new ICommand[commands.length - 1]; - System.arraycopy(commands, 0, newCommands, 0, i); - System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1); - description.setBuildSpec(newCommands); - project.setDescription(description, null); - return true; - } - } - - return false; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ApkInstallManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ApkInstallManager.java deleted file mode 100644 index 172c555..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ApkInstallManager.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * 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.project; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.Device; -import com.android.ddmlib.IDevice; -import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; -import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; -import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; -import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; - -import org.eclipse.core.resources.IProject; - -import java.util.ArrayList; - -/** - * Registers which apk was installed on which device. - *

            - * The goal of this class is to remember the installation of APKs on devices, and provide - * information about whether a new APK should be installed on a device prior to running the - * application from a launch configuration. - *

            - * The manager uses {@link IProject} and {@link IDevice} to identify the target device and the - * (project generating the) APK. This ensures that disconnected and reconnected devices will - * always receive new APKs (since the APK could be uninstalled manually). - *

            - * Manually uninstalling an APK from a connected device will still be a problem, but this should - * be a limited use case. - *

            - * This is a singleton. To get the instance, use {@link #getInstance()} - */ -public class ApkInstallManager implements IDeviceChangeListener, IDebugBridgeChangeListener, - IProjectListener { - - private final static ApkInstallManager sThis = new ApkInstallManager(); - - /** - * Internal struct to associate a project and a device. - */ - private static class ApkInstall { - public ApkInstall(IProject project, IDevice device) { - this.project = project; - this.device = device; - } - IProject project; - IDevice device; - } - - private final ArrayList mInstallList = new ArrayList(); - - public static ApkInstallManager getInstance() { - return sThis; - } - - /** - * Registers an installation of project onto device - * @param project The project that was installed. - * @param device The device that received the installation. - */ - public void registerInstallation(IProject project, IDevice device) { - synchronized (mInstallList) { - mInstallList.add(new ApkInstall(project, device)); - } - } - - /** - * Returns whether a project was installed on the device. - * @param project the project that may have been installed. - * @param device the device that may have received the installation. - * @return - */ - public boolean isApplicationInstalled(IProject project, IDevice device) { - synchronized (mInstallList) { - for (ApkInstall install : mInstallList) { - if (project == install.project && device == install.device) { - return true; - } - } - } - return false; - } - - /** - * Resets registered installations for a specific {@link IProject}. - *

            This ensures that {@link #isApplicationInstalled(IProject, IDevice)} will always return - * null for this specified project, for any device. - * @param project the project for which to reset all installations. - */ - public void resetInstallationFor(IProject project) { - synchronized (mInstallList) { - for (int i = 0 ; i < mInstallList.size() ;) { - ApkInstall install = mInstallList.get(i); - if (install.project == project) { - mInstallList.remove(i); - } else { - i++; - } - } - } - } - - private ApkInstallManager() { - AndroidDebugBridge.addDeviceChangeListener(this); - AndroidDebugBridge.addDebugBridgeChangeListener(this); - ResourceMonitor.getMonitor().addProjectListener(this); - } - - /* - * Responds to a bridge change by clearing the full installation list. - * (non-Javadoc) - * @see com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener#bridgeChanged(com.android.ddmlib.AndroidDebugBridge) - */ - public void bridgeChanged(AndroidDebugBridge bridge) { - // the bridge changed, there is no way to know which IDevice will be which. - // We reset everything - synchronized (mInstallList) { - mInstallList.clear(); - } - } - - /* - * Responds to a device being disconnected by removing all installations related to this device. - * (non-Javadoc) - * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceDisconnected(com.android.ddmlib.Device) - */ - public void deviceDisconnected(Device device) { - synchronized (mInstallList) { - for (int i = 0 ; i < mInstallList.size() ;) { - ApkInstall install = mInstallList.get(i); - if (install.device == device) { - mInstallList.remove(i); - } else { - i++; - } - } - } - } - - /* - * Responds to a close project by resetting all its installation. - * (non-Javadoc) - * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectClosed(org.eclipse.core.resources.IProject) - */ - public void projectClosed(IProject project) { - resetInstallationFor(project); - } - - /* - * Responds to a close project by resetting all its installation. - * (non-Javadoc) - * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectDeleted(org.eclipse.core.resources.IProject) - */ - public void projectDeleted(IProject project) { - resetInstallationFor(project); - } - - /* - * Does nothing - * (non-Javadoc) - * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceChanged(com.android.ddmlib.Device, int) - */ - public void deviceChanged(Device device, int changeMask) { - // nothing to do. - } - - /* - * Does nothing - * (non-Javadoc) - * @see com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener#deviceConnected(com.android.ddmlib.Device) - */ - public void deviceConnected(Device device) { - // nothing to do. - } - - /* - * Does nothing - * (non-Javadoc) - * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectOpened(org.eclipse.core.resources.IProject) - */ - public void projectOpened(IProject project) { - // nothing to do. - } - - /* - * Does nothing - * (non-Javadoc) - * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener#projectOpenedWithWorkspace(org.eclipse.core.resources.IProject) - */ - public void projectOpenedWithWorkspace(IProject project) { - // nothing to do. - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java deleted file mode 100644 index eea8f8e..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AndroidConstants; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IProjectDescription; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.ui.IObjectActionDelegate; -import org.eclipse.ui.IWorkbenchPart; - -import java.util.Iterator; - -/** - * Converts a project created with the activity creator into an - * Android project. - */ -public class ConvertToAndroidAction implements IObjectActionDelegate { - - private ISelection mSelection; - - /* - * (non-Javadoc) - * - * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) - */ - public void setActivePart(IAction action, IWorkbenchPart targetPart) { - // pass - } - - /* - * (non-Javadoc) - * - * @see IActionDelegate#run(IAction) - */ - public void run(IAction action) { - if (mSelection instanceof IStructuredSelection) { - for (Iterator it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { - Object element = it.next(); - IProject project = null; - if (element instanceof IProject) { - project = (IProject)element; - } else if (element instanceof IAdaptable) { - project = (IProject)((IAdaptable)element).getAdapter(IProject.class); - } - if (project != null) { - convertProject(project); - } - } - } - } - - /* - * (non-Javadoc) - * - * @see IActionDelegate#selectionChanged(IAction, ISelection) - */ - public void selectionChanged(IAction action, ISelection selection) { - this.mSelection = selection; - } - - /** - * Toggles sample nature on a project - * - * @param project to have sample nature added or removed - */ - private void convertProject(final IProject project) { - new Job("Convert Project") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - if (monitor != null) { - monitor.beginTask(String.format( - "Convert %1$s to Android", project.getName()), 5); - } - - IProjectDescription description = project.getDescription(); - String[] natures = description.getNatureIds(); - - // check if the project already has the android nature. - for (int i = 0; i < natures.length; ++i) { - if (AndroidConstants.NATURE.equals(natures[i])) { - // we shouldn't be here as the visibility of the item - // is dependent on the project. - return new Status(Status.WARNING, AdtPlugin.PLUGIN_ID, - "Project is already an Android project"); - } - } - - if (monitor != null) { - monitor.worked(1); - } - - String[] newNatures = new String[natures.length + 1]; - System.arraycopy(natures, 0, newNatures, 1, natures.length); - newNatures[0] = AndroidConstants.NATURE; - - // set the new nature list in the project - description.setNatureIds(newNatures); - project.setDescription(description, null); - if (monitor != null) { - monitor.worked(1); - } - - // Fix the classpath entries. - // get a java project - IJavaProject javaProject = JavaCore.create(project); - ProjectHelper.fixProjectClasspathEntries(javaProject); - if (monitor != null) { - monitor.worked(1); - } - - return Status.OK_STATUS; - } catch (JavaModelException e) { - return e.getJavaModelStatus(); - } catch (CoreException e) { - return e.getStatus(); - } finally { - if (monitor != null) { - monitor.done(); - } - } - } - }.schedule(); - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java deleted file mode 100644 index 3d7e929..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.common.project.ExportHelper; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.ui.IObjectActionDelegate; -import org.eclipse.ui.IWorkbenchPart; - -public class ExportAction implements IObjectActionDelegate { - - private ISelection mSelection; - - /** - * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) - */ - public void setActivePart(IAction action, IWorkbenchPart targetPart) { - } - - 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) { - ExportHelper.exportProject(project); - } - } - } - } - - public void selectionChanged(IAction action, ISelection selection) { - this.mSelection = selection; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java deleted file mode 100644 index 9ade490..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.project.export.ExportWizard; - -import org.eclipse.jface.action.IAction; -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) - */ - public void setActivePart(IAction action, IWorkbenchPart targetPart) { - mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); - } - - public void run(IAction action) { - if (mSelection instanceof IStructuredSelection) { - IStructuredSelection selection = (IStructuredSelection)mSelection; - - // 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(); - } - } - - public void selectionChanged(IAction action, ISelection selection) { - this.mSelection = selection; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java deleted file mode 100644 index b216ad4..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.debug.core.DebugPlugin; -import org.eclipse.debug.core.ILaunchConfiguration; -import org.eclipse.debug.core.ILaunchConfigurationType; -import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; -import org.eclipse.debug.core.ILaunchManager; -import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; - -import java.util.ArrayList; - -/** - * Class to fix the launch configuration of a project if the java package - * defined in the manifest has been changed.
            - * This fix can be done synchronously, or asynchronously.
            - * start() will start a thread that will do the fix.
            - * run() will do the fix in the current thread.

            - * By default, the fix first display a dialog to the user asking if he/she wants to - * do the fix. This can be overriden by calling setDisplayPrompt(false). - * - */ -public class FixLaunchConfig extends Thread { - - private IProject mProject; - private String mOldPackage; - private String mNewPackage; - - private boolean mDisplayPrompt = true; - - public FixLaunchConfig(IProject project, String oldPackage, String newPackage) { - super(); - - mProject = project; - mOldPackage = oldPackage; - mNewPackage = newPackage; - } - - /** - * Set the display prompt. If true run()/start() first ask the user if he/she wants - * to fix the Launch Config - * @param displayPrompt - */ - public void setDisplayPrompt(boolean displayPrompt) { - mDisplayPrompt = displayPrompt; - } - - /** - * Fix the Launch configurations. - */ - @Override - public void run() { - - if (mDisplayPrompt) { - // ask the user if he really wants to fix the launch config - boolean res = AdtPlugin.displayPrompt( - "Launch Configuration Update", - "The package definition in the manifest changed.\nDo you want to update your Launch Configuration(s)?"); - - if (res == false) { - return; - } - } - - // get the list of config for the project - String projectName = mProject.getName(); - ILaunchConfiguration[] configs = findConfigs(mProject.getName()); - - // loop through all the config and update the package - for (ILaunchConfiguration config : configs) { - try { - // get the working copy so that we can make changes. - ILaunchConfigurationWorkingCopy copy = config.getWorkingCopy(); - - // get the attributes for the activity - String activity = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, - ""); //$NON-NLS-1$ - - // manifests can define activities that are not in the defined package, - // so we need to make sure the activity is inside the old package. - if (activity.startsWith(mOldPackage)) { - // create the new activity - activity = mNewPackage + activity.substring(mOldPackage.length()); - - // put it in the copy - copy.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, activity); - - // save the config - copy.doSave(); - } - } catch (CoreException e) { - // couldn't get the working copy. we output the error in the console - String msg = String.format("Failed to modify %1$s: %2$s", projectName, - e.getMessage()); - AdtPlugin.printErrorToConsole(mProject, msg); - } - } - - } - - /** - * Looks for and returns all existing Launch Configuration object for a - * specified project. - * @param projectName The name of the project - * @return all the ILaunchConfiguration object. If none are present, an empty array is - * returned. - */ - private static ILaunchConfiguration[] findConfigs(String projectName) { - // get the launch manager - ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); - - // now get the config type for our particular android type. - ILaunchConfigurationType configType = manager. - getLaunchConfigurationType(LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); - - // create a temp list to hold all the valid configs - ArrayList list = new ArrayList(); - - try { - ILaunchConfiguration[] configs = manager.getLaunchConfigurations(configType); - - for (ILaunchConfiguration config : configs) { - if (config.getAttribute( - IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, - "").equals(projectName)) { //$NON-NLS-1$ - list.add(config); - } - } - } catch (CoreException e) { - } - - return list.toArray(new ILaunchConfiguration[list.size()]); - - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java deleted file mode 100644 index 3043818..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.project; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.ui.IObjectActionDelegate; -import org.eclipse.ui.IWorkbenchPart; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.IWorkbenchWindowActionDelegate; - -import java.util.Iterator; - -/** - * Action to fix the project properties: - *

              - *
            • Make sure the framework archive is present with the link to the java - * doc
            • - *
            - */ -public class FixProjectAction implements IObjectActionDelegate { - - private ISelection mSelection; - - /** - * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) - */ - public void setActivePart(IAction action, IWorkbenchPart targetPart) { - } - - public void run(IAction action) { - if (mSelection instanceof IStructuredSelection) { - - for (Iterator it = ((IStructuredSelection) mSelection).iterator(); - it.hasNext();) { - Object element = it.next(); - IProject project = null; - if (element instanceof IProject) { - project = (IProject) element; - } else if (element instanceof IAdaptable) { - project = (IProject) ((IAdaptable) element) - .getAdapter(IProject.class); - } - if (project != null) { - fixProject(project); - } - } - } - } - - public void selectionChanged(IAction action, ISelection selection) { - this.mSelection = selection; - } - - private void fixProject(final IProject project) { - new Job("Fix Project Properties") { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - if (monitor != null) { - monitor.beginTask("Fix Project Properties", 6); - } - - ProjectHelper.fixProject(project); - if (monitor != null) { - monitor.worked(1); - } - - // fix the nature order to have the proper project icon - ProjectHelper.fixProjectNatureOrder(project); - if (monitor != null) { - monitor.worked(1); - } - - // now we fix the builders - AndroidNature.configureResourceManagerBuilder(project); - if (monitor != null) { - monitor.worked(1); - } - - AndroidNature.configurePreBuilder(project); - if (monitor != null) { - monitor.worked(1); - } - - AndroidNature.configureApkBuilder(project); - if (monitor != null) { - monitor.worked(1); - } - - return Status.OK_STATUS; - } catch (JavaModelException e) { - return e.getJavaModelStatus(); - } catch (CoreException e) { - return e.getStatus(); - } finally { - if (monitor != null) { - monitor.done(); - } - } - } - }.schedule(); - } - - /** - * @see IWorkbenchWindowActionDelegate#init - */ - public void init(IWorkbenchWindow window) { - // pass - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java deleted file mode 100644 index ad57a37..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.sdklib.SdkConstants; - -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.viewers.IDecoration; -import org.eclipse.jface.viewers.ILabelDecorator; -import org.eclipse.jface.viewers.ILabelProviderListener; -import org.eclipse.jface.viewers.ILightweightLabelDecorator; - -/** - * A {@link ILabelDecorator} associated with an org.eclipse.ui.decorators extension. - * This is used to add android icons in some special folders in the package explorer. - */ -public class FolderDecorator implements ILightweightLabelDecorator { - - private ImageDescriptor mDescriptor; - - public FolderDecorator() { - mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); //$NON-NLS-1$ - } - - public void decorate(Object element, IDecoration decoration) { - if (element instanceof IFolder) { - IFolder folder = (IFolder)element; - - // get the project and make sure this is an android project - IProject project = folder.getProject(); - - try { - if (project.hasNature(AndroidConstants.NATURE)) { - // check the folder is directly under the project. - if (folder.getParent().getType() == IResource.PROJECT) { - String name = folder.getName(); - if (name.equals(SdkConstants.FD_ASSETS)) { - doDecoration(decoration, null); - } else if (name.equals(SdkConstants.FD_RESOURCES)) { - doDecoration(decoration, null); - } else if (name.equals(SdkConstants.FD_GEN_SOURCES)) { - doDecoration(decoration, " [Generated Java Files]"); - } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { - doDecoration(decoration, null); - } - } - } - } catch (CoreException e) { - // log the error - AdtPlugin.log(e, "Unable to get nature of project '%s'.", project.getName()); - } - } - } - - public void doDecoration(IDecoration decoration, String suffix) { - decoration.addOverlay(mDescriptor, IDecoration.TOP_LEFT); - - if (suffix != null) { - decoration.addSuffix(suffix); - - // this is broken as it changes the color of the whole text, not only of the decoration. - // TODO: figure out how to change the color of the decoration only. -// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); -// ColorRegistry registry = theme.getColorRegistry(); -// decoration.setForegroundColor( -// registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations")); //$NON-NLS-1$ - } - - } - - public boolean isLabelProperty(Object element, String property) { - // Property change do not affect the label - return false; - } - - public void addListener(ILabelProviderListener listener) { - // No state change will affect the rendering. - } - - public void removeListener(ILabelProviderListener listener) { - // No state change will affect the rendering. - } - - public void dispose() { - // nothing to dispose - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java deleted file mode 100644 index ad2eb53..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java +++ /dev/null @@ -1,771 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; -import com.android.ide.eclipse.common.project.AndroidManifestParser; -import com.android.ide.eclipse.common.project.BaseProjectHelper; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IMarker; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IProjectDescription; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.core.resources.IncrementalProjectBuilder; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.QualifiedName; -import org.eclipse.jdt.core.IClasspathEntry; -import org.eclipse.jdt.core.IJavaModel; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.launching.JavaRuntime; - -import java.util.ArrayList; -import java.util.List; - -/** - * Utility class to manipulate Project parameters/properties. - */ -public final class ProjectHelper { - public final static int COMPILER_COMPLIANCE_OK = 0; - public final static int COMPILER_COMPLIANCE_LEVEL = 1; - public final static int COMPILER_COMPLIANCE_SOURCE = 2; - public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3; - - /** - * Adds the corresponding source folder to the class path entries. - * - * @param entries The class path entries to read. A copy will be returned. - * @param new_entry The parent source folder to remove. - * @return A new class path entries array. - */ - public static IClasspathEntry[] addEntryToClasspath( - IClasspathEntry[] entries, IClasspathEntry new_entry) { - int n = entries.length; - IClasspathEntry[] newEntries = new IClasspathEntry[n + 1]; - System.arraycopy(entries, 0, newEntries, 0, n); - newEntries[n] = new_entry; - return newEntries; - } - - /** - * Adds the corresponding source folder to the project's class path entries. - * - * @param javaProject The java project of which path entries to update. - * @param new_entry The parent source folder to remove. - * @throws JavaModelException - */ - public static void addEntryToClasspath( - IJavaProject javaProject, IClasspathEntry new_entry) - throws JavaModelException { - - IClasspathEntry[] entries = javaProject.getRawClasspath(); - entries = addEntryToClasspath(entries, new_entry); - javaProject.setRawClasspath(entries, new NullProgressMonitor()); - } - - /** - * Remove a classpath entry from the array. - * @param entries The class path entries to read. A copy will be returned - * @param index The index to remove. - * @return A new class path entries array. - */ - public static IClasspathEntry[] removeEntryFromClasspath( - IClasspathEntry[] entries, int index) { - int n = entries.length; - IClasspathEntry[] newEntries = new IClasspathEntry[n-1]; - - // copy the entries before index - System.arraycopy(entries, 0, newEntries, 0, index); - - // copy the entries after index - System.arraycopy(entries, index + 1, newEntries, index, - entries.length - index - 1); - - return newEntries; - } - - /** - * Converts a OS specific path into a path valid for the java doc location - * attributes of a project. - * @param javaDocOSLocation The OS specific path. - * @return a valid path for the java doc location. - */ - public static String getJavaDocPath(String javaDocOSLocation) { - // first thing we do is convert the \ into / - String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$ - AndroidConstants.WS_SEP); - - // then we add file: at the beginning for unix path, and file:/ for non - // unix path - if (javaDoc.startsWith(AndroidConstants.WS_SEP)) { - return "file:" + javaDoc; //$NON-NLS-1$ - } - - return "file:/" + javaDoc; //$NON-NLS-1$ - } - - /** - * Look for a specific classpath entry by full path and return its index. - * @param entries The entry array to search in. - * @param entryPath The OS specific path of the entry. - * @param entryKind The kind of the entry. Accepted values are 0 - * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, - * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, - * and IClasspathEntry.CPE_CONTAINER - * @return the index of the found classpath entry or -1. - */ - public static int findClasspathEntryByPath(IClasspathEntry[] entries, - String entryPath, int entryKind) { - for (int i = 0 ; i < entries.length ; i++) { - IClasspathEntry entry = entries[i]; - - int kind = entry.getEntryKind(); - - if (kind == entryKind || entryKind == 0) { - // get the path - IPath path = entry.getPath(); - - String osPathString = path.toOSString(); - if (osPathString.equals(entryPath)) { - return i; - } - } - } - - // not found, return bad index. - return -1; - } - - /** - * Look for a specific classpath entry for file name only and return its - * index. - * @param entries The entry array to search in. - * @param entryName The filename of the entry. - * @param entryKind The kind of the entry. Accepted values are 0 - * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, - * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, - * and IClasspathEntry.CPE_CONTAINER - * @param startIndex Index where to start the search - * @return the index of the found classpath entry or -1. - */ - public static int findClasspathEntryByName(IClasspathEntry[] entries, - String entryName, int entryKind, int startIndex) { - if (startIndex < 0) { - startIndex = 0; - } - for (int i = startIndex ; i < entries.length ; i++) { - IClasspathEntry entry = entries[i]; - - int kind = entry.getEntryKind(); - - if (kind == entryKind || entryKind == 0) { - // get the path - IPath path = entry.getPath(); - String name = path.segment(path.segmentCount()-1); - - if (name.equals(entryName)) { - return i; - } - } - } - - // not found, return bad index. - return -1; - } - - /** - * Fix the project. This checks the SDK location. - * @param project The project to fix. - * @throws JavaModelException - */ - public static void fixProject(IProject project) throws JavaModelException { - if (AdtPlugin.getOsSdkFolder().length() == 0) { - AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed."); - return; - } - - // get a java project - IJavaProject javaProject = JavaCore.create(project); - fixProjectClasspathEntries(javaProject); - } - - /** - * Fix the project classpath entries. The method ensures that: - *
              - *
            • The project does not reference any old android.zip/android.jar archive.
            • - *
            • The project does not use its output folder as a sourc folder.
            • - *
            • The project does not reference a desktop JRE
            • - *
            • The project references the AndroidClasspathContainer. - *
            - * @param javaProject The project to fix. - * @throws JavaModelException - */ - public static void fixProjectClasspathEntries(IJavaProject javaProject) - throws JavaModelException { - - // get the project classpath - IClasspathEntry[] entries = javaProject.getRawClasspath(); - IClasspathEntry[] oldEntries = entries; - - // check if the JRE is set as library - int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER, - IClasspathEntry.CPE_CONTAINER); - if (jreIndex != -1) { - // the project has a JRE included, we remove it - entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex); - } - - // get the output folder - IPath outputFolder = javaProject.getOutputLocation(); - - boolean foundContainer = false; - - for (int i = 0 ; i < entries.length ;) { - // get the entry and kind - IClasspathEntry entry = entries[i]; - int kind = entry.getEntryKind(); - - if (kind == IClasspathEntry.CPE_SOURCE) { - IPath path = entry.getPath(); - - if (path.equals(outputFolder)) { - entries = ProjectHelper.removeEntryFromClasspath(entries, i); - - // continue, to skip the i++; - continue; - } - } else if (kind == IClasspathEntry.CPE_CONTAINER) { - if (AndroidClasspathContainerInitializer.checkPath(entry.getPath())) { - foundContainer = true; - } - } - - i++; - } - - // if the framework container is not there, we add it - if (foundContainer == false) { - // add the android container to the array - entries = ProjectHelper.addEntryToClasspath(entries, - AndroidClasspathContainerInitializer.getContainerEntry()); - } - - // set the new list of entries to the project - if (entries != oldEntries) { - javaProject.setRawClasspath(entries, new NullProgressMonitor()); - } - - // If needed, check and fix compiler compliance and source compatibility - ProjectHelper.checkAndFixCompilerCompliance(javaProject); - } - - - /** - * Checks the project compiler compliance level is supported. - * @param javaProject The project to check - * @return
              - *
            • COMPILER_COMPLIANCE_OK if the project is properly configured
            • - *
            • COMPILER_COMPLIANCE_LEVEL for unsupported compiler level
            • - *
            • COMPILER_COMPLIANCE_SOURCE for unsupported source compatibility
            • - *
            • COMPILER_COMPLIANCE_CODEGEN_TARGET for unsupported .class format
            • - *
            - */ - public static final int checkCompilerCompliance(IJavaProject javaProject) { - // get the project compliance level option - String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); - - // check it against a list of valid compliance level strings. - if (checkCompliance(compliance) == false) { - // if we didn't find the proper compliance level, we return an error - return COMPILER_COMPLIANCE_LEVEL; - } - - // otherwise we check source compatibility - String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true); - - // check it against a list of valid compliance level strings. - if (checkCompliance(source) == false) { - // if we didn't find the proper compliance level, we return an error - return COMPILER_COMPLIANCE_SOURCE; - } - - // otherwise check codegen level - String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true); - - // check it against a list of valid compliance level strings. - if (checkCompliance(codeGen) == false) { - // if we didn't find the proper compliance level, we return an error - return COMPILER_COMPLIANCE_CODEGEN_TARGET; - } - - return COMPILER_COMPLIANCE_OK; - } - - /** - * Checks the project compiler compliance level is supported. - * @param project The project to check - * @return
              - *
            • COMPILER_COMPLIANCE_OK if the project is properly configured
            • - *
            • COMPILER_COMPLIANCE_LEVEL for unsupported compiler level
            • - *
            • COMPILER_COMPLIANCE_SOURCE for unsupported source compatibility
            • - *
            • COMPILER_COMPLIANCE_CODEGEN_TARGET for unsupported .class format
            • - *
            - */ - public static final int checkCompilerCompliance(IProject project) { - // get the java project from the IProject resource object - IJavaProject javaProject = JavaCore.create(project); - - // check and return the result. - return checkCompilerCompliance(javaProject); - } - - - /** - * Checks, and fixes if needed, the compiler compliance level, and the source compatibility - * level - * @param project The project to check and fix. - */ - public static final void checkAndFixCompilerCompliance(IProject project) { - // get the java project from the IProject resource object - IJavaProject javaProject = JavaCore.create(project); - - // Now we check the compiler compliance level and make sure it is valid - checkAndFixCompilerCompliance(javaProject); - } - - /** - * Checks, and fixes if needed, the compiler compliance level, and the source compatibility - * level - * @param javaProject The Java project to check and fix. - */ - public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) { - if (checkCompilerCompliance(javaProject) != COMPILER_COMPLIANCE_OK) { - // setup the preferred compiler compliance level. - javaProject.setOption(JavaCore.COMPILER_COMPLIANCE, - AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); - javaProject.setOption(JavaCore.COMPILER_SOURCE, - AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); - javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, - AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); - - // clean the project to make sure we recompile - try { - javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD, - new NullProgressMonitor()); - } catch (CoreException e) { - AdtPlugin.printErrorToConsole(javaProject.getProject(), - "Project compiler settings changed. Clean your project."); - } - } - } - - /** - * Returns a {@link IProject} by its running application name, as it returned by the AVD. - *

            - * applicationName will in most case be the package declared in the manifest, but - * can, in some cases, be a custom process name declared in the manifest, in the - * application, activity, receiver, or - * service nodes. - * @param applicationName The application name. - * @return a project or null if no matching project were found. - */ - public static IProject findAndroidProjectByAppName(String applicationName) { - // Get the list of project for the current workspace - IWorkspace workspace = ResourcesPlugin.getWorkspace(); - IProject[] projects = workspace.getRoot().getProjects(); - - // look for a project that matches the packageName of the app - // we're trying to debug - for (IProject p : projects) { - if (p.isOpen()) { - try { - if (p.hasNature(AndroidConstants.NATURE) == false) { - // ignore non android projects - continue; - } - } catch (CoreException e) { - // failed to get the nature? skip project. - continue; - } - - // check that there is indeed a manifest file. - IFile manifestFile = AndroidManifestParser.getManifest(p); - if (manifestFile == null) { - // no file? skip this project. - continue; - } - - AndroidManifestParser parser = null; - try { - parser = AndroidManifestParser.parseForData(manifestFile); - } catch (CoreException e) { - // ignore, handled below. - } - if (parser == null) { - // skip this project. - continue; - } - - String manifestPackage = parser.getPackage(); - - if (manifestPackage != null && manifestPackage.equals(applicationName)) { - // this is the project we were looking for! - return p; - } else { - // if the package and application name don't match, - // we look for other possible process names declared in the manifest. - String[] processes = parser.getProcesses(); - for (String process : processes) { - if (process.equals(applicationName)) { - return p; - } - } - } - } - } - - return null; - - } - - public static void fixProjectNatureOrder(IProject project) throws CoreException { - IProjectDescription description = project.getDescription(); - String[] natures = description.getNatureIds(); - - // if the android nature is not the first one, we reorder them - if (AndroidConstants.NATURE.equals(natures[0]) == false) { - // look for the index - for (int i = 0 ; i < natures.length ; i++) { - if (AndroidConstants.NATURE.equals(natures[i])) { - // if we try to just reorder the array in one pass, this doesn't do - // anything. I guess JDT check that we are actually adding/removing nature. - // So, first we'll remove the android nature, and then add it back. - - // remove the android nature - removeNature(project, AndroidConstants.NATURE); - - // now add it back at the first index. - description = project.getDescription(); - natures = description.getNatureIds(); - - String[] newNatures = new String[natures.length + 1]; - - // first one is android - newNatures[0] = AndroidConstants.NATURE; - - // next the rest that was before the android nature - System.arraycopy(natures, 0, newNatures, 1, natures.length); - - // set the new natures - description.setNatureIds(newNatures); - project.setDescription(description, null); - - // and stop - break; - } - } - } - } - - - /** - * Removes a specific nature from a project. - * @param project The project to remove the nature from. - * @param nature The nature id to remove. - * @throws CoreException - */ - public static void removeNature(IProject project, String nature) throws CoreException { - IProjectDescription description = project.getDescription(); - String[] natures = description.getNatureIds(); - - // check if the project already has the android nature. - for (int i = 0; i < natures.length; ++i) { - if (nature.equals(natures[i])) { - String[] newNatures = new String[natures.length - 1]; - if (i > 0) { - System.arraycopy(natures, 0, newNatures, 0, i); - } - System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1); - description.setNatureIds(newNatures); - project.setDescription(description, null); - - return; - } - } - - } - - /** - * Returns if the project has error level markers. - * @param includeReferencedProjects flag to also test the referenced projects. - * @throws CoreException - */ - public static boolean hasError(IProject project, boolean includeReferencedProjects) - throws CoreException { - IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); - if (markers != null && markers.length > 0) { - // the project has marker(s). even though they are "problem" we - // don't know their severity. so we loop on them and figure if they - // are warnings or errors - for (IMarker m : markers) { - int s = m.getAttribute(IMarker.SEVERITY, -1); - if (s == IMarker.SEVERITY_ERROR) { - return true; - } - } - } - - // test the referenced projects if needed. - if (includeReferencedProjects) { - IProject[] projects = getReferencedProjects(project); - - for (IProject p : projects) { - if (hasError(p, false)) { - return true; - } - } - } - - return false; - } - - /** - * Saves a String property into the persistent storage of a resource. - * @param resource The resource into which the string value is saved. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @param value the value to save - * @return true if the save succeeded. - */ - public static boolean saveStringProperty(IResource resource, String propertyName, - String value) { - QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); - - try { - resource.setPersistentProperty(qname, value); - } catch (CoreException e) { - return false; - } - - return true; - } - - /** - * Loads a String property from the persistent storage of a resource. - * @param resource The resource from which the string value is loaded. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @return the property value or null if it was not found. - */ - public static String loadStringProperty(IResource resource, String propertyName) { - QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); - - try { - String value = resource.getPersistentProperty(qname); - return value; - } catch (CoreException e) { - return null; - } - } - - /** - * Saves a property into the persistent storage of a resource. - * @param resource The resource into which the boolean value is saved. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @param value the value to save - * @return true if the save succeeded. - */ - public static boolean saveBooleanProperty(IResource resource, String propertyName, - boolean value) { - return saveStringProperty(resource, propertyName, Boolean.toString(value)); - } - - /** - * Loads a boolean property from the persistent storage of the project. - * @param resource The resource from which the boolean value is loaded. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @param defaultValue The default value to return if the property was not found. - * @return the property value or the default value if the property was not found. - */ - public static boolean loadBooleanProperty(IResource resource, String propertyName, - boolean defaultValue) { - String value = loadStringProperty(resource, propertyName); - if (value != null) { - return Boolean.parseBoolean(value); - } - - return defaultValue; - } - - /** - * Saves the path of a resource into the persistent storate of the project. - * @param resource The resource into which the resource path is saved. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @param value The resource to save. It's its path that is actually stored. If null, an - * empty string is stored. - * @return true if the save succeeded - */ - public static boolean saveResourceProperty(IResource resource, String propertyName, - IResource value) { - if (value != null) { - IPath iPath = value.getProjectRelativePath(); - return saveStringProperty(resource, propertyName, iPath.toString()); - } - - return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$ - } - - /** - * Loads the path of a resource from the persistent storage of the project, and returns the - * corresponding IResource object, if it exists in the same project as resource. - * @param resource The resource from which the resource path is loaded. - * @param propertyName the name of the property. The id of the plugin is added to this string. - * @return The corresponding IResource object (or children interface) or null - */ - public static IResource loadResourceProperty(IResource resource, String propertyName) { - String value = loadStringProperty(resource, propertyName); - - if (value != null && value.length() > 0) { - return resource.getProject().findMember(value); - } - - return null; - } - - /** - * Returns the list of referenced project that are opened and Java projects. - * @param project - * @return list of opened referenced java project. - * @throws CoreException - */ - public static IProject[] getReferencedProjects(IProject project) throws CoreException { - IProject[] projects = project.getReferencedProjects(); - - ArrayList list = new ArrayList(); - - for (IProject p : projects) { - if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { - list.add(p); - } - } - - return list.toArray(new IProject[list.size()]); - } - - - /** - * Checks a Java project compiler level option against a list of supported versions. - * @param optionValue the Compiler level option. - * @return true if the option value is supproted. - */ - private static boolean checkCompliance(String optionValue) { - for (String s : AndroidConstants.COMPILER_COMPLIANCE) { - if (s != null && s.equals(optionValue)) { - return true; - } - } - - return false; - } - - /** - * Returns the apk filename for the given project - * @param project The project. - * @param config An optional config name. Can be null. - */ - public static String getApkFilename(IProject project, String config) { - if (config != null) { - return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$ - } - - return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; - } - - /** - * Find the list of projects on which this JavaProject is dependent on at the compilation level. - * - * @param javaProject Java project that we are looking for the dependencies. - * @return A list of Java projects for which javaProject depend on. - * @throws JavaModelException - */ - public static List getAndroidProjectDependencies(IJavaProject javaProject) - throws JavaModelException { - String[] requiredProjectNames = javaProject.getRequiredProjectNames(); - - // Go from java project name to JavaProject name - IJavaModel javaModel = javaProject.getJavaModel(); - - // loop through all dependent projects and keep only those that are Android projects - List projectList = new ArrayList(requiredProjectNames.length); - for (String javaProjectName : requiredProjectNames) { - IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName); - - //Verify that the project has also the Android Nature - try { - if (!androidJavaProject.getProject().hasNature(AndroidConstants.NATURE)) { - continue; - } - } catch (CoreException e) { - continue; - } - - projectList.add(androidJavaProject); - } - - return projectList; - } - - /** - * Returns the android package file as an IFile object for the specified - * project. - * @param project The project - * @return The android package as an IFile object or null if not found. - */ - public static IFile getApplicationPackage(IProject project) { - // get the output folder - IFolder outputLocation = BaseProjectHelper.getOutputFolder(project); - - if (outputLocation == null) { - AdtPlugin.printErrorToConsole(project, - "Failed to get the output location of the project. Check build path properties" - ); - return null; - } - - - // get the package path - String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; - IResource r = outputLocation.findMember(packageName); - - // check the package is present - if (r instanceof IFile && r.exists()) { - return (IFile)r; - } - - String msg = String.format("Could not find %1$s!", packageName); - AdtPlugin.printErrorToConsole(project, msg); - - return null; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java deleted file mode 100644 index 6ede10d..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java +++ /dev/null @@ -1,570 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.common.project.BaseProjectHelper; -import com.android.jarutils.KeystoreHelper; -import com.android.jarutils.SignedJarBuilder; -import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; -import com.android.jarutils.DebugKeyProvider.KeytoolException; - -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IncrementalProjectBuilder; -import org.eclipse.core.runtime.CoreException; -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.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.KeyStore.PrivateKeyEntry; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; - -/** - * 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_large.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 String PROPERTY_FILENAME = "baseFilename"; //$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() { - 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 mDestinationParentFolder; - - private ExportWizardPage mKeystoreSelectionPage; - private ExportWizardPage mKeyCreationPage; - private ExportWizardPage mKeySelectionPage; - private ExportWizardPage mKeyCheckPage; - - private boolean mKeyCreationMode; - - private List mExistingAliases; - - private Map mApkMap; - - 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, - mDestinationParentFolder.getAbsolutePath()); - ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME, - mApkMap.get(null)[APK_FILE_DEST]); - - // 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 - */ - 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 { - // first we make sure the project is built - mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor); - - // if needed, create the keystore and/or key. - if (mKeystoreCreationMode || mKeyCreationMode) { - final ArrayList output = new ArrayList(); - boolean createdStore = KeystoreHelper.createNewStore( - mKeystore, - null /*storeType*/, - mKeystorePassword, - mKeyAlias, - mKeyPassword, - mDName, - mValidity, - new IKeyGenOutput() { - public void err(String message) { - output.add(message); - } - 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(); - } 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) { - // get the output folder of the project to export. - // this is where we'll find the built apks to resign and export. - IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject); - if (outputIFolder == null) { - return false; - } - String outputOsPath = outputIFolder.getLocation().toOSString(); - - // now generate the packages. - Set> set = mApkMap.entrySet(); - for (Entry entry : set) { - String[] defaultApk = entry.getValue(); - String srcFilename = defaultApk[APK_FILE_SOURCE]; - String destFilename = defaultApk[APK_FILE_DEST]; - - FileOutputStream fos = new FileOutputStream( - new File(mDestinationParentFolder, destFilename)); - SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); - - // get the input file. - FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename)); - - // add the content of the source file to the output file, and sign it at - // the same time. - try { - builder.writeZip(fis, null /* filter */); - // close the builder: write the final signature files, and close the archive. - builder.close(); - } finally { - try { - fis.close(); - } finally { - fos.close(); - } - } - } - return true; - } - } catch (FileNotFoundException e) { - displayError(e); - } catch (NoSuchAlgorithmException e) { - displayError(e); - } catch (IOException e) { - displayError(e); - } catch (GeneralSecurityException e) { - displayError(e); - } catch (KeytoolException e) { - displayError(e); - } catch (CoreException e) { - displayError(e); - } - - 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 mApkMap != null && mApkMap.size() > 0 && - ((mPrivateKey != null && mCertificate != null) - || mKeystoreCreationMode || mKeyCreationMode) && - mDestinationParentFolder != null; - } - - /* - * (non-Javadoc) - * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection) - */ - 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 aliases) { - mExistingAliases = aliases; - } - - List 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; - } - - void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { - mPrivateKey = privateKey; - mCertificate = certificate; - } - - void setDestination(File parentFolder, Map apkMap) { - mDestinationParentFolder = parentFolder; - mApkMap = apkMap; - } - - void resetDestination() { - mDestinationParentFolder = null; - mApkMap = 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(Exception e) { - String message = getExceptionMessage(e); - displayError(message); - - AdtPlugin.log(e, "Export Wizard Error"); - } - - /** - * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns - * null, the method is called again on the cause of the Throwable object. - *

            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) { - Throwable cause = t.getCause(); - if (cause != null) { - return getExceptionMessage(cause); - } - - // no more cause and still no message. display the first exception. - return t.getClass().getCanonicalName(); - } - - return message; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java deleted file mode 100644 index 7fd76e9..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java +++ /dev/null @@ -1,443 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; -import com.android.ide.eclipse.adt.sdk.Sdk; - -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.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.UnrecoverableEntryException; -import java.security.KeyStore.PrivateKeyEntry; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; - -/** - * Final page of the wizard that checks the key and ask for the ouput location. - */ -final class KeyCheckPage extends ExportWizardPage { - - private final ExportWizard mWizard; - private PrivateKey mPrivateKey; - private X509Certificate mCertificate; - private Text mDestination; - private boolean mFatalSigningError; - private FormText mDetailText; - /** The Apk Config map for the current project */ - private Map mApkConfig; - 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 - } - - 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() { - 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(); - mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project); - - String destination = ProjectHelper.loadStringProperty(project, - ExportWizard.PROPERTY_DESTINATION); - String filename = ProjectHelper.loadStringProperty(project, - ExportWizard.PROPERTY_FILENAME); - if (destination != null && filename != null) { - mDestination.setText(destination + File.separator + filename); - } - } - - // 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("

            Certificate expires in %d years.

            ", - validity)); - - if (validity < 25) { - sb.append("

            Make sure the certificate is valid for the planned lifetime of the product.

            "); - sb.append("

            If the certificate expires, you will be forced to sign your application with a different one.

            "); - sb.append("

            Applications cannot be upgraded if their certificate changes from one version to another, "); - sb.append("forcing a full uninstall/install, which will make the user lose his/her data.

            "); - sb.append("

            Android Market currently requires certificates to be valid until 2033.

            "); - } - - 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( - "

            Certificate expired on %s

            ", - 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( - "

            Certificate expires on %s.

            ", - mCertificate.getNotAfter().toString())); - - int expirationYear = expirationCalendar.get(Calendar.YEAR); - int thisYear = today.get(Calendar.YEAR); - - if (thisYear + 25 < expirationYear) { - // do nothing - } else { - if (expirationYear == thisYear) { - sb.append("

            The certificate expires this year.

            "); - } else { - int count = expirationYear-thisYear; - sb.append(String.format( - "

            The Certificate expires in %1$s %2$s.

            ", - count, count == 1 ? "year" : "years")); - } - - sb.append("

            Make sure the certificate is valid for the planned lifetime of the product.

            "); - sb.append("

            If the certificate expires, you will be forced to sign your application with a different one.

            "); - sb.append("

            Applications cannot be upgraded if their certificate changes from one version to another, "); - sb.append("forcing a full uninstall/install, which will make the user lose his/her data.

            "); - sb.append("

            Android Market currently requires certificates to be valid until 2033.

            "); - } - - 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; - } - - // display the list of files that will actually be created - Map apkFileMap = getApkFileMap(file); - - // display them - boolean fileExists = false; - StringBuilder sb = new StringBuilder(String.format( - "

            This will create the following files:

            ")); - - Set> set = apkFileMap.entrySet(); - for (Entry entry : set) { - String[] apkArray = entry.getValue(); - String filename = apkArray[ExportWizard.APK_FILE_DEST]; - File f = new File(parentFolder, filename); - if (f.isFile()) { - fileExists = true; - sb.append(String.format("
          • %1$s (WARNING: already exists)
          • ", filename)); - } else if (f.isDirectory()) { - setErrorMessage(String.format("%1$s is a directory.", filename)); - // reset canFinish in the wizard. - mWizard.resetDestination(); - setPageComplete(false); - return; - } else { - sb.append(String.format("
          • %1$s
          • ", filename)); - } - } - - mDestinationDetails = sb.toString(); - - // no error, set the destination in the wizard. - mWizard.setDestination(parentFolder, apkFileMap); - setPageComplete(true); - - // However, we should also test if the file already exists. - if (fileExists) { - setMessage("A destination file already exists.", WARNING); - } - - 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("
            "); - if (mKeyDetails != null) { - sb.append(mKeyDetails); - } - - if (mDestinationDetails != null && mFatalSigningError == false) { - sb.append(mDestinationDetails); - } - - sb.append("
            "); - - mDetailText.setText(sb.toString(), true /* parseTags */, - true /* expandURLs */); - - mDetailText.getParent().layout(); - - updateScrolling(); - - } - - /** - * Creates the list of destination filenames based on the content of the destination field - * and the list of APK configurations for the project. - * - * @param file File name from the destination field - * @return A list of destination filenames based file and the list of APK - * configurations for the project. - */ - private Map getApkFileMap(File file) { - String filename = file.getName(); - - HashMap map = new HashMap(); - - // add the default APK filename - String[] apkArray = new String[ExportWizard.APK_COUNT]; - apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( - mWizard.getProject(), null /*config*/); - apkArray[ExportWizard.APK_FILE_DEST] = filename; - map.put(null, apkArray); - - // add the APKs for each APK configuration. - if (mApkConfig != null && mApkConfig.size() > 0) { - // remove the extension. - int index = filename.lastIndexOf('.'); - String base = filename.substring(0, index); - String extension = filename.substring(index); - - Set> set = mApkConfig.entrySet(); - for (Entry entry : set) { - apkArray = new String[ExportWizard.APK_COUNT]; - apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( - mWizard.getProject(), entry.getKey()); - apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension; - map.put(entry.getKey(), apkArray); - } - } - - return map; - } - - @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/project/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java deleted file mode 100644 index d7365f7..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.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 mExistingAliases; - - - protected KeyCreationPage(ExportWizard wizard, String pageName) { - super(pageName); - mWizard = wizard; - - setTitle("Key Creation"); - setDescription(""); // TODO? - } - - 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() { - 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() { - public void modifyText(ModifyEvent e) { - mWizard.setKeyAlias(mAlias.getText().trim()); - onChange(); - } - }); - mKeyPassword.addModifyListener(new ModifyListener() { - public void modifyText(ModifyEvent e) { - mWizard.setKeyPassword(mKeyPassword.getText()); - onChange(); - } - }); - mKeyPassword2.addModifyListener(new ModifyListener() { - public void modifyText(ModifyEvent e) { - onChange(); - } - }); - - validityText.addModifyListener(new ModifyListener() { - 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() { - 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/project/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java deleted file mode 100644 index 2fcd757..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.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 - } - - 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() { - 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 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 aliasList = new ArrayList(); - - 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/project/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java deleted file mode 100644 index c5a4d47..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.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 - } - - 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() { - public void modifyText(ModifyEvent e) { - mWizard.setKeystore(mKeystore.getText().trim()); - onChange(); - } - }); - - mKeystorePassword.addModifyListener(new ModifyListener() { - public void modifyText(ModifyEvent e) { - mWizard.setKeystorePassword(mKeystorePassword.getText()); - onChange(); - } - }); - - mKeystorePassword2.addModifyListener(new ModifyListener() { - 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/project/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java deleted file mode 100644 index 123b7cb..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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.project.export; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; -import com.android.ide.eclipse.common.project.AndroidManifestParser; -import com.android.ide.eclipse.common.project.BaseProjectHelper; -import com.android.ide.eclipse.common.project.ProjectChooserHelper; - -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.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Text; - -import java.io.File; - -/** - * 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 Display mDisplay; - 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."); - } - - public void createControl(Composite parent) { - mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); - mDisplay = parent.getDisplay(); - - 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() { - 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()); - - 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(AndroidConstants.NATURE) == 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.getOutputFolder(project); - if (outputIFolder != null) { - String outputOsPath = outputIFolder.getLocation().toOSString(); - String apkFilePath = outputOsPath + File.separator + project.getName() + - AndroidConstants.DOT_ANDROID_PACKAGE; - - File f = new File(apkFilePath); - if (f.isFile() == false) { - addError(mErrorComposite, - String.format("%1$s/%2$s/%1$s%3$s does not exists!", - project.getName(), - outputIFolder.getName(), - AndroidConstants.DOT_ANDROID_PACKAGE)); - } - } else { - addError(mErrorComposite, - "Unable to get the output folder of the project!"); - } - - - // project is an android project, we check the debuggable attribute. - AndroidManifestParser manifestParser = AndroidManifestParser.parse( - BaseProjectHelper.getJavaProject(project), null /* errorListener */, - true /* gatherData */, false /* markErrors */); - - Boolean debuggable = manifestParser.getDebuggable(); - - if (debuggable != null && debuggable == Boolean.TRUE) { - addWarning(mErrorComposite, - "The manifest 'debuggable' attribute is set to true.\nYou should set it to false for applications that you release to the public."); - } - - // 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 = AdtPlugin.getImageLoader().loadImage(IMG_ERROR, mDisplay); - } - - 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 = AdtPlugin.getImageLoader().loadImage(IMG_WARNING, mDisplay); - } - - 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/project/internal/AndroidClasspathContainer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java deleted file mode 100644 index c7cb427..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.project.internal; - -import org.eclipse.core.runtime.IPath; -import org.eclipse.jdt.core.IClasspathContainer; -import org.eclipse.jdt.core.IClasspathEntry; - -/** - * Classpath container for the Android projects. - */ -class AndroidClasspathContainer implements IClasspathContainer { - - private IClasspathEntry[] mClasspathEntry; - private IPath mContainerPath; - private String mName; - - /** - * Constructs the container with the {@link IClasspathEntry} representing the android - * framework jar file and the container id - * @param entries the entries representing the android framework and optional libraries. - * @param path the path containing the classpath container id. - * @param name the name of the container to display. - */ - AndroidClasspathContainer(IClasspathEntry[] entries, IPath path, String name) { - mClasspathEntry = entries; - mContainerPath = path; - mName = name; - } - - public IClasspathEntry[] getClasspathEntries() { - return mClasspathEntry; - } - - public String getDescription() { - return mName; - } - - public int getKind() { - return IClasspathContainer.K_DEFAULT_SYSTEM; - } - - public IPath getPath() { - return mContainerPath; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java deleted file mode 100644 index b1e79b7..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java +++ /dev/null @@ -1,646 +0,0 @@ -/* - * 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.project.internal; - -import com.android.ide.eclipse.adt.AdtConstants; -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.sdk.LoadStatus; -import com.android.ide.eclipse.adt.sdk.Sdk; -import com.android.ide.eclipse.common.project.BaseProjectHelper; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.IAndroidTarget.IOptionalLibrary; - -import org.eclipse.core.resources.IMarker; -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.NullProgressMonitor; -import org.eclipse.core.runtime.Path; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jdt.core.ClasspathContainerInitializer; -import org.eclipse.jdt.core.IAccessRule; -import org.eclipse.jdt.core.IClasspathAttribute; -import org.eclipse.jdt.core.IClasspathContainer; -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 java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.regex.Pattern; - -/** - * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to - * {@link IProject}s. This removes the hard-coded path to the android.jar. - */ -public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { - /** The container id for the android framework jar file */ - private final static String CONTAINER_ID = - "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ - - /** path separator to store multiple paths in a single property. This is guaranteed to not - * be in a path. - */ - private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ - - private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ - private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ - private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ - private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; - - private final static int CACHE_INDEX_JAR = 0; - private final static int CACHE_INDEX_SRC = 1; - private final static int CACHE_INDEX_DOCS_URI = 2; - private final static int CACHE_INDEX_OPT_DOCS_URI = 3; - private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; - - public AndroidClasspathContainerInitializer() { - // pass - } - - /** - * Binds a classpath container to a {@link IClasspathContainer} for a given project, - * or silently fails if unable to do so. - * @param containerPath the container path that is the container id. - * @param project the project to bind - */ - @Override - public void initialize(IPath containerPath, IJavaProject project) throws CoreException { - if (CONTAINER_ID.equals(containerPath.toString())) { - JavaCore.setClasspathContainer(new Path(CONTAINER_ID), - new IJavaProject[] { project }, - new IClasspathContainer[] { allocateAndroidContainer(project) }, - new NullProgressMonitor()); - } - } - - /** - * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER} - * linking to the Android Framework. - */ - public static IClasspathEntry getContainerEntry() { - return JavaCore.newContainerEntry(new Path(CONTAINER_ID)); - } - - /** - * Checks the {@link IPath} objects against the android framework container id and - * returns true if they are identical. - * @param path the IPath to check. - */ - public static boolean checkPath(IPath path) { - return CONTAINER_ID.equals(path.toString()); - } - - /** - * Updates the {@link IJavaProject} objects with new android framework container. This forces - * JDT to recompile them. - * @param androidProjects the projects to update. - * @return true if success, false otherwise. - */ - public static boolean updateProjects(IJavaProject[] androidProjects) { - try { - // Allocate a new AndroidClasspathContainer, and associate it to the android framework - // container id for each projects. - // By providing a new association between a container id and a IClasspathContainer, - // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with - // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of - // the projects. - int projectCount = androidProjects.length; - - IClasspathContainer[] containers = new IClasspathContainer[projectCount]; - for (int i = 0 ; i < projectCount; i++) { - containers[i] = allocateAndroidContainer(androidProjects[i]); - } - - // give each project their new container in one call. - JavaCore.setClasspathContainer( - new Path(CONTAINER_ID), - androidProjects, containers, new NullProgressMonitor()); - - return true; - } catch (JavaModelException e) { - return false; - } - } - - /** - * Allocates and returns an {@link AndroidClasspathContainer} object with the proper - * path to the framework jar file. - * @param javaProject The java project that will receive the container. - */ - private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { - final IProject iProject = javaProject.getProject(); - - String markerMessage = null; - boolean outputToConsole = true; - - try { - AdtPlugin plugin = AdtPlugin.getDefault(); - - // get the lock object for project manipulation during SDK load. - Object lock = plugin.getSdkLockObject(); - synchronized (lock) { - boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; - - // check if the project has a valid target. - IAndroidTarget target = null; - if (sdkIsLoaded) { - target = Sdk.getCurrent().getTarget(iProject); - } - - // if we are loaded and the target is non null, we create a valid ClassPathContainer - if (sdkIsLoaded && target != null) { - - String targetName = target.getClasspathName(); - - return new AndroidClasspathContainer( - createClasspathEntries(iProject, target, targetName), - new Path(CONTAINER_ID), targetName); - } - - // In case of error, we'll try different thing to provide the best error message - // possible. - // Get the project's target's hash string (if it exists) - String hashString = Sdk.getProjectTargetHashString(iProject); - - if (hashString == null || hashString.length() == 0) { - // if there is no hash string we only show this if the SDK is loaded. - // For a project opened at start-up with no target, this would be displayed - // twice, once when the project is opened, and once after the SDK has - // finished loading. - // By testing the sdk is loaded, we only show this once in the console. - if (sdkIsLoaded) { - markerMessage = String.format( - "Project has no target set. Edit the project properties to set one."); - } - } else if (sdkIsLoaded) { - markerMessage = String.format( - "Unable to resolve target '%s'", hashString); - } else { - // this is the case where there is a hashString but the SDK is not yet - // loaded and therefore we can't get the target yet. - // We check if there is a cache of the needed information. - AndroidClasspathContainer container = getContainerFromCache(iProject); - - if (container == null) { - // either the cache was wrong (ie folder does not exists anymore), or - // there was no cache. In this case we need to make sure the project - // is resolved again after the SDK is loaded. - plugin.setProjectToResolve(javaProject); - - markerMessage = String.format( - "Unable to resolve target '%s' until the SDK is loaded.", - hashString); - - // let's not log this one to the console as it will happen at every boot, - // and it's expected. (we do keep the error marker though). - outputToConsole = false; - - } else { - // we created a container from the cache, so we register the project - // to be checked for cache validity once the SDK is loaded - plugin.setProjectToCheck(javaProject); - - // and return the container - return container; - } - - } - - // return a dummy container to replace the one we may have had before. - // It'll be replaced by the real when if/when the target is resolved if/when the - // SDK finishes loading. - return new IClasspathContainer() { - public IClasspathEntry[] getClasspathEntries() { - return new IClasspathEntry[0]; - } - - public String getDescription() { - return "Unable to get system library for the project"; - } - - public int getKind() { - return IClasspathContainer.K_DEFAULT_SYSTEM; - } - - public IPath getPath() { - return null; - } - }; - } - } finally { - if (markerMessage != null) { - // log the error and put the marker on the project if we can. - if (outputToConsole) { - AdtPlugin.printErrorToConsole(iProject, markerMessage); - } - - try { - BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage, - -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); - } catch (CoreException e) { - // In some cases, the workspace may be locked for modification when we - // pass here. - // We schedule a new job to put the marker after. - final String fmessage = markerMessage; - Job markerJob = new Job("Android SDK: Resolving error markers") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, - fmessage, -1, IMarker.SEVERITY_ERROR, - IMarker.PRIORITY_HIGH); - } catch (CoreException e2) { - return e2.getStatus(); - } - - return Status.OK_STATUS; - } - }; - - // build jobs are run after other interactive jobs - markerJob.setPriority(Job.BUILD); - markerJob.schedule(); - } - } else { - // no error, remove potential MARKER_TARGETs. - try { - if (iProject.exists()) { - iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, - IResource.DEPTH_INFINITE); - } - } catch (CoreException ce) { - // In some cases, the workspace may be locked for modification when we pass - // here, so we schedule a new job to put the marker after. - Job markerJob = new Job("Android SDK: Resolving error markers") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, - IResource.DEPTH_INFINITE); - } catch (CoreException e2) { - return e2.getStatus(); - } - - return Status.OK_STATUS; - } - }; - - // build jobs are run after other interactive jobs - markerJob.setPriority(Job.BUILD); - markerJob.schedule(); - } - } - } - } - - /** - * Creates and returns an array of {@link IClasspathEntry} objects for the android - * framework and optional libraries. - *

            This references the OS path to the android.jar and the - * java doc directory. This is dynamically created when a project is opened, - * and never saved in the project itself, so there's no risk of storing an - * obsolete path. - * The method also stores the paths used to create the entries in the project persistent - * properties. A new {@link AndroidClasspathContainer} can be created from the stored path - * using the {@link #getContainerFromCache(IProject)} method. - * @param project - * @param target The target that contains the libraries. - * @param targetName - */ - private static IClasspathEntry[] createClasspathEntries(IProject project, - IAndroidTarget target, String targetName) { - - // get the path from the target - String[] paths = getTargetPaths(target); - - // create the classpath entry from the paths - IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); - - // paths now contains all the path required to recreate the IClasspathEntry with no - // target info. We encode them in a single string, with each path separated by - // OS path separator. - StringBuilder sb = new StringBuilder(CACHE_VERSION); - for (String p : paths) { - sb.append(PATH_SEPARATOR); - sb.append(p); - } - - // store this in a project persistent property - ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); - ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); - - return entries; - } - - /** - * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. - */ - private static AndroidClasspathContainer getContainerFromCache(IProject project) { - // get the cached info from the project persistent properties. - String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); - String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); - if (cache == null || targetNameCache == null) { - return null; - } - - // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. - if (cache.startsWith(CACHE_VERSION_SEP) == false) { - return null; - } - - cache = cache.substring(CACHE_VERSION_SEP.length()); - - // the cache contains multiple paths, separated by a character guaranteed to not be in - // the path (\u001C). - // The first 3 are for android.jar (jar, source, doc), the rest are for the optional - // libraries and should contain at least one doc and a jar (if there are any libraries). - // Therefore, the path count should be 3 or 5+ - String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); - if (paths.length < 3 || paths.length == 4) { - return null; - } - - // now we check the paths actually exist. - // There's an exception: If the source folder for android.jar does not exist, this is - // not a problem, so we skip it. - // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a - // bit differently. - try { - if (new File(paths[CACHE_INDEX_JAR]).exists() == false || - new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { - return null; - } - - // check the path for the add-ons, if they exist. - if (paths.length > CACHE_INDEX_ADD_ON_START) { - - // check the docs path separately from the rest of the paths as it's a URI. - if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { - return null; - } - - // now just check the remaining paths. - for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { - String path = paths[i]; - if (path.length() > 0) { - File f = new File(path); - if (f.exists() == false) { - return null; - } - } - } - } - } catch (URISyntaxException e) { - return null; - } - - IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); - - return new AndroidClasspathContainer(entries, - new Path(CONTAINER_ID), targetNameCache); - } - - /** - * Generates an array of {@link IClasspathEntry} from a set of paths. - * @see #getTargetPaths(IAndroidTarget) - */ - private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { - ArrayList list = new ArrayList(); - - // First, we create the IClasspathEntry for the framework. - // now add the android framework to the class path. - // create the path object. - IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); - IPath android_src = new Path(paths[CACHE_INDEX_SRC]); - - // create the java doc link. - IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( - IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, - paths[CACHE_INDEX_DOCS_URI]); - - // create the access rule to restrict access to classes in com.android.internal - IAccessRule accessRule = JavaCore.newAccessRule( - new Path("com/android/internal/**"), //$NON-NLS-1$ - IAccessRule.K_NON_ACCESSIBLE); - - IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib, - android_src, // source attachment path - null, // default source attachment root path. - new IAccessRule[] { accessRule }, - new IClasspathAttribute[] { cpAttribute }, - false // not exported. - ); - - list.add(frameworkClasspathEntry); - - // now deal with optional libraries - if (paths.length >= 5) { - String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; - int i = 4; - while (i < paths.length) { - Path jarPath = new Path(paths[i++]); - - IClasspathAttribute[] attributes = null; - if (docPath.length() > 0) { - attributes = new IClasspathAttribute[] { - JavaCore.newClasspathAttribute( - IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, - docPath) - }; - } - - IClasspathEntry entry = JavaCore.newLibraryEntry( - jarPath, - null, // source attachment path - null, // default source attachment root path. - null, - attributes, - false // not exported. - ); - list.add(entry); - } - } - - return list.toArray(new IClasspathEntry[list.size()]); - } - - /** - * Checks the projects' caches. If the cache was valid, the project is removed from the list. - * @param projects the list of projects to check. - */ - public static void checkProjectsCache(ArrayList projects) { - int i = 0; - projectLoop: while (i < projects.size()) { - IJavaProject javaProject = projects.get(i); - IProject iProject = javaProject.getProject(); - - // check if the project is opened - if (iProject.isOpen() == false) { - // remove from the list - // we do not increment i in this case. - projects.remove(i); - - continue; - } - - // get the target from the project and its paths - IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); - if (target == null) { - // this is really not supposed to happen. This would mean there are cached paths, - // but default.properties was deleted. Keep the project in the list to force - // a resolve which will display the error. - i++; - continue; - } - - String[] targetPaths = getTargetPaths(target); - - // now get the cached paths - String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); - if (cache == null) { - // this should not happen. We'll force resolve again anyway. - i++; - continue; - } - - String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); - if (cachedPaths.length < 3 || cachedPaths.length == 4) { - // paths length is wrong. simply resolve the project again - i++; - continue; - } - - // Now we compare the paths. The first 4 can be compared directly. - // because of case sensitiveness we need to use File objects - - if (targetPaths.length != cachedPaths.length) { - // different paths, force resolve again. - i++; - continue; - } - - // compare the main paths (android.jar, main sources, main javadoc) - if (new File(targetPaths[CACHE_INDEX_JAR]).equals( - new File(cachedPaths[CACHE_INDEX_JAR])) == false || - new File(targetPaths[CACHE_INDEX_SRC]).equals( - new File(cachedPaths[CACHE_INDEX_SRC])) == false || - new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( - new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { - // different paths, force resolve again. - i++; - continue; - } - - if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { - // compare optional libraries javadoc - if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( - new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { - // different paths, force resolve again. - i++; - continue; - } - - // testing the optional jar files is a little bit trickier. - // The order is not guaranteed to be identical. - // From a previous test, we do know however that there is the same number. - // The number of libraries should be low enough that we can simply go through the - // lists manually. - targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { - String targetPath = targetPaths[tpi]; - - // look for a match in the other array - for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { - if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { - // found a match. Try the next targetPath - continue targetLoop; - } - } - - // if we stop here, we haven't found a match, which means there's a - // discrepancy in the libraries. We force a resolve. - i++; - continue projectLoop; - } - } - - // at the point the check passes, and we can remove the project from the list. - // we do not increment i in this case. - projects.remove(i); - } - } - - /** - * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. - *

            The paths are always in the same order. - *

              - *
            • Path to android.jar
            • - *
            • Path to the source code for android.jar
            • - *
            • Path to the javadoc for the android platform
            • - *
            - * Additionally, if there are optional libraries, the array will contain: - *
              - *
            • Path to the librairies javadoc
            • - *
            • Path to the first .jar file
            • - *
            • (more .jar as needed)
            • - *
            - */ - private static String[] getTargetPaths(IAndroidTarget target) { - ArrayList paths = new ArrayList(); - - // first, we get the path for android.jar - // The order is: android.jar, source folder, docs folder - paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); - paths.add(target.getPath(IAndroidTarget.SOURCES)); - paths.add(AdtPlugin.getUrlDoc()); - - // now deal with optional libraries. - IOptionalLibrary[] libraries = target.getOptionalLibraries(); - if (libraries != null) { - // all the optional libraries use the same javadoc, so we start with this - String targetDocPath = target.getPath(IAndroidTarget.DOCS); - if (targetDocPath != null) { - paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); - } else { - // we add an empty string, to always have the same count. - paths.add(""); - } - - // because different libraries could use the same jar file, we make sure we add - // each jar file only once. - HashSet visitedJars = new HashSet(); - for (IOptionalLibrary library : libraries) { - String jarPath = library.getJarPath(); - if (visitedJars.contains(jarPath) == false) { - visitedJars.add(jarPath); - paths.add(jarPath); - } - } - } - - return paths.toArray(new String[paths.size()]); - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java deleted file mode 100644 index c9568b2..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.project.properties; - -import com.android.ide.eclipse.adt.sdk.Sdk; -import com.android.sdklib.IAndroidTarget; -import com.android.sdkuilib.ApkConfigWidget; -import com.android.sdkuilib.SdkTargetSelector; - -import org.eclipse.core.resources.IProject; -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.Control; -import org.eclipse.swt.widgets.Label; -import org.eclipse.ui.IWorkbenchPropertyPage; -import org.eclipse.ui.dialogs.PropertyPage; - -import java.util.Map; - -/** - * Property page for "Android" project. - * This is accessible from the Package Explorer when right clicking a project and choosing - * "Properties". - * - */ -public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPropertyPage { - - private IProject mProject; - private SdkTargetSelector mSelector; - private ApkConfigWidget mApkConfigWidget; - - public AndroidPropertyPage() { - // pass - } - - @Override - protected Control createContents(Composite parent) { - // get the element (this is not yet valid in the constructor). - mProject = (IProject)getElement(); - - // get the targets from the sdk - IAndroidTarget[] targets = null; - if (Sdk.getCurrent() != null) { - targets = Sdk.getCurrent().getTargets(); - } - - // build the UI. - Composite top = new Composite(parent, SWT.NONE); - top.setLayoutData(new GridData(GridData.FILL_BOTH)); - top.setLayout(new GridLayout(1, false)); - - Label l = new Label(top, SWT.NONE); - l.setText("Project Build Target"); - - mSelector = new SdkTargetSelector(top, targets); - - l = new Label(top, SWT.SEPARATOR | SWT.HORIZONTAL); - l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - l = new Label(top, SWT.NONE); - l.setText("Project APK Configurations"); - - mApkConfigWidget = new ApkConfigWidget(top); - - // fill the ui - Sdk currentSdk = Sdk.getCurrent(); - if (currentSdk != null && mProject.isOpen()) { - // get the target - IAndroidTarget target = currentSdk.getTarget(mProject); - if (target != null) { - mSelector.setSelection(target); - } - - // get the apk configurations - Map configs = currentSdk.getProjectApkConfigs(mProject); - mApkConfigWidget.fillTable(configs); - } - - mSelector.setSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - // look for the selection and validate the page if there is a selection - IAndroidTarget target = mSelector.getSelected(); - setValid(target != null); - } - }); - - if (mProject.isOpen() == false) { - // disable the ui. - } - - return top; - } - - @Override - public boolean performOk() { - Sdk currentSdk = Sdk.getCurrent(); - if (currentSdk != null) { - currentSdk.setProject(mProject, mSelector.getSelected(), - mApkConfigWidget.getApkConfigs()); - } - - return true; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java deleted file mode 100644 index 99f4836..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import com.android.ide.eclipse.adt.AndroidConstants; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.jdt.core.ICompilationUnit; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.text.ITextSelection; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.ltk.ui.refactoring.RefactoringWizard; -import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; -import org.eclipse.ui.IEditorInput; -import org.eclipse.ui.IEditorPart; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.IWorkbenchWindowActionDelegate; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.part.FileEditorInput; - -/* - * Quick Reference Link: - * http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html - * and - * http://www.ibm.com/developerworks/opensource/library/os-ecjdt/ - */ - -/** - * Action executed when the "Extract String" menu item is invoked. - *

            - * The intent of the action is to start a refactoring that extracts a source string and - * replaces it by an Android string resource ID. - *

            - * Workflow: - *

              - *
            • The action is currently located in the Refactoring menu in the main menu. - *
            • TODO: extend the popup refactoring menu in a Java or Android XML file. - *
            • The action is only enabled if the selection is 1 character or more. That is at least part - * of the string must be selected, it's not enough to just move the insertion point. This is - * a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when - * the insertion point is merely moved. TODO: address this limitation. - *
                The action gets the current {@link ISelection}. It also knows the current - * {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the - * actual resource file. By looking at the Active Window > Active Page > Active Editor we - * can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file) - * that is being edited. - *
                  TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit} - * can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow - * us to be able to work with a selection from an Android XML file later. - *
                • The action creates a new {@link ExtractStringRefactoring} and make it run on in a new - * {@link ExtractStringWizard}. - *
                    - */ -public class ExtractStringAction implements IWorkbenchWindowActionDelegate { - - /** Keep track of the current workbench window. */ - private IWorkbenchWindow mWindow; - private ITextSelection mSelection; - private IFile mFile; - - /** - * Keep track of the current workbench window. - */ - public void init(IWorkbenchWindow window) { - mWindow = window; - } - - public void dispose() { - // Nothing to do - } - - /** - * Examine the selection to determine if the action should be enabled or not. - *

                    - * Keep a link to the relevant selection structure (i.e. a part of the Java AST). - */ - public void selectionChanged(IAction action, ISelection selection) { - - // Note, two kinds of selections are returned here: - // ITextSelection on a Java source window - // IStructuredSelection in the outline or navigator - // This simply deals with the refactoring based on a non-empty selection. - // At that point, just enable the action and later decide if it's valid when it actually - // runs since we don't have access to the AST yet. - - mSelection = null; - mFile = null; - - if (selection instanceof ITextSelection) { - mSelection = (ITextSelection) selection; - if (mSelection.getLength() > 0) { - mFile = getSelectedFile(); - } - } - - action.setEnabled(mSelection != null && mFile != null); - } - - /** - * Create a new instance of our refactoring and a wizard to configure it. - */ - public void run(IAction action) { - if (mSelection != null && mFile != null) { - ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mSelection); - RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject()); - RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); - try { - op.run(mWindow.getShell(), wizard.getDefaultPageTitle()); - } catch (InterruptedException e) { - // Interrupted. Pass. - } - } - } - - /** - * Returns the active {@link IFile} (hopefully matching our selection) or null. - * The file is only returned if it's a file from a project with an Android nature. - *

                    - * At that point we do not try to analyze if the selection nor the file is suitable - * for the refactoring. This check is performed when the refactoring is invoked since - * it can then produce meaningful error messages as needed. - */ - private IFile getSelectedFile() { - IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); - if (wwin != null) { - IWorkbenchPage page = wwin.getActivePage(); - if (page != null) { - IEditorPart editor = page.getActiveEditor(); - if (editor != null) { - IEditorInput input = editor.getEditorInput(); - - if (input instanceof FileEditorInput) { - FileEditorInput fi = (FileEditorInput) input; - IFile file = fi.getFile(); - if (file.exists()) { - IProject proj = file.getProject(); - try { - if (proj != null && proj.hasNature(AndroidConstants.NATURE)) { - return file; - } - } catch (CoreException e) { - // ignore - } - } - } - } - } - } - - return null; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java deleted file mode 100644 index 465e1a3..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import org.eclipse.ltk.core.refactoring.RefactoringContribution; -import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; - -import java.util.Map; - -/** - * @see ExtractStringDescriptor - */ -public class ExtractStringContribution extends RefactoringContribution { - - /* (non-Javadoc) - * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int) - */ - @SuppressWarnings("unchecked") - @Override - public RefactoringDescriptor createDescriptor( - String id, - String project, - String description, - String comment, - Map arguments, - int flags) - throws IllegalArgumentException { - return new ExtractStringDescriptor(project, description, comment, arguments); - } - - @SuppressWarnings("unchecked") - @Override - public Map retrieveArgumentMap(RefactoringDescriptor descriptor) { - if (descriptor instanceof ExtractStringDescriptor) { - return ((ExtractStringDescriptor) descriptor).getArguments(); - } - return super.retrieveArgumentMap(descriptor); - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java deleted file mode 100644 index 6e999e9..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.ltk.core.refactoring.Refactoring; -import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; - -import java.util.Map; - -/** - * A descriptor that allows an {@link ExtractStringRefactoring} to be created from - * a previous instance of itself. - */ -public class ExtractStringDescriptor extends RefactoringDescriptor { - - public static final String ID = - "com.android.ide.eclipse.adt.refactoring.extract.string"; //$NON-NLS-1$ - - private final Map mArguments; - - public ExtractStringDescriptor(String project, String description, String comment, - Map arguments) { - super(ID, project, description, comment, - RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags - ); - mArguments = arguments; - } - - public Map getArguments() { - return mArguments; - } - - /** - * Creates a new refactoring instance for this refactoring descriptor based on - * an argument map. The argument map is created by the refactoring itself in - * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)} - *

                    - * This is apparently used to replay a refactoring. - * - * {@inheritDoc} - * - * @throws CoreException - */ - @Override - public Refactoring createRefactoring(RefactoringStatus status) throws CoreException { - try { - ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments); - return ref; - } catch (NullPointerException e) { - status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor"); - return null; - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java deleted file mode 100644 index b3e8e5c..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * 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.refactorings.extractstring; - - -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.ui.ConfigurationSelector; -import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; -import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; -import com.android.sdklib.SdkConstants; - -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.jface.wizard.IWizardPage; -import org.eclipse.jface.wizard.WizardPage; -import org.eclipse.ltk.ui.refactoring.UserInputWizardPage; -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.Combo; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Group; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Text; - -import java.util.HashMap; -import java.util.TreeSet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @see ExtractStringRefactoring - */ -class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage { - - /** Last res file path used, shared across the session instances but specific to the - * current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */ - private static HashMap sLastResFilePath = new HashMap(); - - /** The project where the user selection happened. */ - private final IProject mProject; - - /** Test field where the user enters the new ID to be generated or replaced with. */ - private Text mStringIdField; - /** The configuration selector, to select the resource path of the XML file. */ - private ConfigurationSelector mConfigSelector; - /** The combo to display the existing XML files or enter a new one. */ - private Combo mResFileCombo; - - /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and - * a leaf file name ending with .xml */ - private static final Pattern RES_XML_FILE_REGEX = Pattern.compile( - "/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$ - /** Absolute destination folder root, e.g. "/res/" */ - private static final String RES_FOLDER_ABS = - AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP; - /** Relative destination folder root, e.g. "res/" */ - private static final String RES_FOLDER_REL = - SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; - - private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml"; //$NON-NLS-1$ - - private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); - - public ExtractStringInputPage(IProject project) { - super("ExtractStringInputPage"); //$NON-NLS-1$ - mProject = project; - } - - /** - * Create the UI for the refactoring wizard. - *

                    - * Note that at that point the initial conditions have been checked in - * {@link ExtractStringRefactoring}. - */ - public void createControl(Composite parent) { - Composite content = new Composite(parent, SWT.NONE); - GridLayout layout = new GridLayout(); - layout.numColumns = 1; - content.setLayout(layout); - - createStringGroup(content); - createResFileGroup(content); - - validatePage(); - setControl(content); - } - - /** - * Creates the top group with the field to replace which string and by what - * and by which options. - * - * @param content A composite with a 1-column grid layout - */ - public void createStringGroup(Composite content) { - - final ExtractStringRefactoring ref = getOurRefactoring(); - - Group group = new Group(content, SWT.NONE); - group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { - group.setText("String Replacement"); - } else { - group.setText("New String"); - } - - GridLayout layout = new GridLayout(); - layout.numColumns = 2; - group.setLayout(layout); - - // line: Textfield for string value (based on selection, if any) - - Label label = new Label(group, SWT.NONE); - label.setText("String"); - - String selectedString = ref.getTokenString(); - - final Text stringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER); - stringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - stringValueField.setText(selectedString != null ? selectedString : ""); //$NON-NLS-1$ - - ref.setNewStringValue(stringValueField.getText()); - - stringValueField.addModifyListener(new ModifyListener() { - public void modifyText(ModifyEvent e) { - if (validatePage()) { - ref.setNewStringValue(stringValueField.getText()); - } - } - }); - - - // TODO provide an option to replace all occurences of this string instead of - // just the one. - - // line : Textfield for new ID - - label = new Label(group, SWT.NONE); - if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { - label.setText("Replace by R.string."); - } else if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { - label.setText("New R.string."); - } else { - label.setText("ID R.string."); - } - - mStringIdField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER); - mStringIdField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mStringIdField.setText(guessId(selectedString)); - - ref.setNewStringId(mStringIdField.getText().trim()); - - mStringIdField.addModifyListener(new ModifyListener() { - public void modifyText(ModifyEvent e) { - if (validatePage()) { - ref.setNewStringId(mStringIdField.getText().trim()); - } - } - }); - } - - /** - * Creates the lower group with the fields to choose the resource confirmation and - * the target XML file. - * - * @param content A composite with a 1-column grid layout - */ - private void createResFileGroup(Composite content) { - - Group group = new Group(content, SWT.NONE); - group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - group.setText("XML resource to edit"); - - GridLayout layout = new GridLayout(); - layout.numColumns = 2; - group.setLayout(layout); - - // line: selection of the res config - - Label label; - label = new Label(group, SWT.NONE); - label.setText("Configuration:"); - - mConfigSelector = new ConfigurationSelector(group); - GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); - gd.horizontalSpan = 2; - gd.widthHint = ConfigurationSelector.WIDTH_HINT; - gd.heightHint = ConfigurationSelector.HEIGHT_HINT; - mConfigSelector.setLayoutData(gd); - OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated(); - mConfigSelector.setOnChangeListener(onConfigSelectorUpdated); - - // line: selection of the output file - - label = new Label(group, SWT.NONE); - label.setText("Resource file:"); - - mResFileCombo = new Combo(group, SWT.DROP_DOWN); - mResFileCombo.select(0); - mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mResFileCombo.addModifyListener(onConfigSelectorUpdated); - - // set output file name to the last one used - - String projPath = mProject.getFullPath().toPortableString(); - String filePath = sLastResFilePath.get(projPath); - - mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH); - onConfigSelectorUpdated.run(); - } - - /** - * Utility method to guess a suitable new XML ID based on the selected string. - */ - private String guessId(String text) { - if (text == null) { - return ""; //$NON-NLS-1$ - } - - // make lower case - text = text.toLowerCase(); - - // everything not alphanumeric becomes an underscore - text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - - // the id must be a proper Java identifier, so it can't start with a number - if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) { - text = "_" + text; //$NON-NLS-1$ - } - return text; - } - - /** - * Returns the {@link ExtractStringRefactoring} instance used by this wizard page. - */ - private ExtractStringRefactoring getOurRefactoring() { - return (ExtractStringRefactoring) getRefactoring(); - } - - /** - * Validates fields of the wizard input page. Displays errors as appropriate and - * enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}. - * - * @return True if the page has been positively validated. It may still have warnings. - */ - private boolean validatePage() { - boolean success = true; - - // Analyze fatal errors. - - String text = mStringIdField.getText().trim(); - if (text == null || text.length() < 1) { - setErrorMessage("Please provide a resource ID."); - success = false; - } else { - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - boolean ok = i == 0 ? - Character.isJavaIdentifierStart(c) : - Character.isJavaIdentifierPart(c); - if (!ok) { - setErrorMessage(String.format( - "The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.", - c, i+1)); - success = false; - break; - } - } - } - - String resFile = mResFileCombo.getText(); - if (success) { - if (resFile == null || resFile.length() == 0) { - setErrorMessage("A resource file name is required."); - success = false; - } else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) { - setErrorMessage("The XML file name is not valid."); - success = false; - } - } - - // Analyze info & warnings. - - if (success) { - setErrorMessage(null); - - ExtractStringRefactoring ref = getOurRefactoring(); - - ref.setTargetFile(resFile); - sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile); - - if (mXmlHelper.isResIdDuplicate(mProject, resFile, text)) { - String msg = String.format("There's already a string item called '%1$s' in %2$s.", - text, resFile); - if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { - setErrorMessage(msg); - success = false; - } else { - setMessage(msg, WizardPage.WARNING); - } - } else if (mProject.findMember(resFile) == null) { - setMessage( - String.format("File %2$s does not exist and will be created.", - text, resFile), - WizardPage.INFORMATION); - } else { - setMessage(null); - } - } - - setPageComplete(success); - return success; - } - - public class OnConfigSelectorUpdated implements Runnable, ModifyListener { - - /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */ - private final Pattern mPathRegex = Pattern.compile( - "(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$ - - /** Temporary config object used to retrieve the Config Selector value. */ - private FolderConfiguration mTempConfig = new FolderConfiguration(); - - private HashMap> mFolderCache = - new HashMap>(); - private String mLastFolderUsedInCombo = null; - private boolean mInternalConfigChange; - private boolean mInternalFileComboChange; - - /** - * Callback invoked when the {@link ConfigurationSelector} has been changed. - *

                    - * The callback does the following: - *

                      - *
                    • Examine the current file name to retrieve the XML filename, if any. - *
                    • Recompute the path based on the configuration selector (e.g. /res/values-fr/). - *
                    • Examine the path to retrieve all the files in it. Keep those in a local cache. - *
                    • If the XML filename from step 1 is not in the file list, it's a custom file name. - * Insert it and sort it. - *
                    • Re-populate the file combo with all the choices. - *
                    • Select the original XML file. - */ - public void run() { - if (mInternalConfigChange) { - return; - } - - // get current leafname, if any - String leafName = ""; //$NON-NLS-1$ - String currPath = mResFileCombo.getText(); - Matcher m = mPathRegex.matcher(currPath); - if (m.matches()) { - // Note: groups 1 and 2 cannot be null. - leafName = m.group(2); - currPath = m.group(1); - } else { - // There was a path but it was invalid. Ignore it. - currPath = ""; //$NON-NLS-1$ - } - - // recreate the res path from the current configuration - mConfigSelector.getConfiguration(mTempConfig); - StringBuffer sb = new StringBuffer(RES_FOLDER_ABS); - sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES)); - sb.append('/'); - - String newPath = sb.toString(); - if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) { - // Path has not changed. No need to reload. - return; - } - - // Get all the files at the new path - - TreeSet filePaths = mFolderCache.get(newPath); - - if (filePaths == null) { - filePaths = new TreeSet(); - - IFolder folder = mProject.getFolder(newPath); - if (folder != null && folder.exists()) { - try { - for (IResource res : folder.members()) { - String name = res.getName(); - if (res.getType() == IResource.FILE && name.endsWith(".xml")) { - filePaths.add(newPath + name); - } - } - } catch (CoreException e) { - // Ignore. - } - } - - mFolderCache.put(newPath, filePaths); - } - - currPath = newPath + leafName; - if (leafName.length() > 0 && !filePaths.contains(currPath)) { - filePaths.add(currPath); - } - - // Fill the combo - try { - mInternalFileComboChange = true; - - mResFileCombo.removeAll(); - - for (String filePath : filePaths) { - mResFileCombo.add(filePath); - } - - int index = -1; - if (leafName.length() > 0) { - index = mResFileCombo.indexOf(currPath); - if (index >= 0) { - mResFileCombo.select(index); - } - } - - if (index == -1) { - mResFileCombo.setText(currPath); - } - - mLastFolderUsedInCombo = newPath; - - } finally { - mInternalFileComboChange = false; - } - - // finally validate the whole page - validatePage(); - } - - /** - * Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been - * modified. - */ - public void modifyText(ModifyEvent e) { - if (mInternalFileComboChange) { - return; - } - - String wsFolderPath = mResFileCombo.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()); - - mInternalFileComboChange = true; - mResFileCombo.setText(wsFolderPath); - mInternalFileComboChange = false; - } - - if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { - wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); - - int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR); - if (pos >= 0) { - wsFolderPath = wsFolderPath.substring(0, pos); - } - - String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP); - - if (folderSegments.length > 0) { - String folderName = folderSegments[0]; - - if (folderName != null && !folderName.equals(wsFolderPath)) { - // update config selector - mInternalConfigChange = true; - mConfigSelector.setConfiguration(folderSegments); - mInternalConfigChange = false; - } - } - } - - validatePage(); - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java deleted file mode 100644 index f7d8334..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java +++ /dev/null @@ -1,988 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestParser; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.ResourceAttributes; -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.Path; -import org.eclipse.core.runtime.SubMonitor; -import org.eclipse.jdt.core.IBuffer; -import org.eclipse.jdt.core.ICompilationUnit; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.core.ToolFactory; -import org.eclipse.jdt.core.compiler.IScanner; -import org.eclipse.jdt.core.compiler.ITerminalSymbols; -import org.eclipse.jdt.core.compiler.InvalidInputException; -import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.ASTVisitor; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.Name; -import org.eclipse.jdt.core.dom.QualifiedName; -import org.eclipse.jdt.core.dom.SimpleName; -import org.eclipse.jdt.core.dom.StringLiteral; -import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; -import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; -import org.eclipse.jface.text.ITextSelection; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.ltk.core.refactoring.ChangeDescriptor; -import org.eclipse.ltk.core.refactoring.CompositeChange; -import org.eclipse.ltk.core.refactoring.Refactoring; -import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; -import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; -import org.eclipse.ltk.core.refactoring.TextFileChange; -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.eclipse.text.edits.TextEditGroup; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * This refactoring extracts a string from a file and replaces it by an Android resource ID - * such as R.string.foo. - *

                      - * There are a number of scenarios, which are not all supported yet. The workflow works as - * such: - *

                        - *
                      • User selects a string in a Java (TODO: or XML file) and invokes - * the {@link ExtractStringAction}. - *
                      • The action finds the {@link ICompilationUnit} being edited as well as the current - * {@link ITextSelection}. The action creates a new instance of this refactoring as - * well as an {@link ExtractStringWizard} and runs the operation. - *
                      • TODO: to support refactoring from an XML file, the action should give the {@link IFile} - * and then here we would have to determine whether it's a suitable Android XML file or a - * suitable Java file. - * TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout - * files or text elements (e.g. foo) for values, etc. - *
                      • Step 1 of the refactoring is to check the preliminary conditions. Right now we check - * that the java source is not read-only and is in sync. We also try to find a string under - * the selection. If this fails, the refactoring is aborted. - *
                      • TODO: Find the string in an XML file based on selection. - *
                      • On success, the wizard is shown, which let the user input the new ID to use. - *
                      • The wizard sets the user input values into this refactoring instance, e.g. the new string - * ID, the XML file to update, etc. The wizard does use the utility method - * {@link XmlStringFileHelper#isResIdDuplicate(IProject, String, String)} to check whether - * the new ID is already defined in the target XML file. - *
                      • Once Preview or Finish is selected in the wizard, the - * {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input - * and compute the actual changes. - *
                      • When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked. - *
                      - * - * The list of changes are: - *
                        - *
                      • If the target XML does not exist, create it with the new string ID. - *
                      • If the target XML exists, find the node and add the new string ID right after. - * If the node is , it needs to be opened. - *
                      • Create an AST rewriter to edit the source Java file and replace all occurences by the - * new computed R.string.foo. Also need to rewrite imports to import R as needed. - * If there's already a conflicting R included, we need to insert the FQCN instead. - *
                      • TODO: If the source is an XML file, determine if we need to change an attribute or a - * a text element. - *
                      • TODO: Have a pref in the wizard: [x] Change other XML Files - *
                      • TODO: Have a pref in the wizard: [x] Change other Java Files - *
                      - */ -public class ExtractStringRefactoring extends Refactoring { - - public enum Mode { - /** - * the Extract String refactoring is called on an existing source file. - * Its purpose is then to get the selected string of the source and propose to - * change it by an XML id. The XML id may be a new one or an existing one. - */ - EDIT_SOURCE, - /** - * The Extract String refactoring is called without any source file. - * Its purpose is then to create a new XML string ID or select/modify an existing one. - */ - SELECT_ID, - /** - * The Extract String refactoring is called without any source file. - * Its purpose is then to create a new XML string ID. The ID must not already exist. - */ - SELECT_NEW_ID - } - - /** The {@link Mode} of operation of the refactoring. */ - private final Mode mMode; - /** The file model being manipulated. - * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ - private final IFile mFile; - /** The project that contains {@link #mFile} and that contains the target XML file to modify. */ - private final IProject mProject; - /** The start of the selection in {@link #mFile}. - * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ - private final int mSelectionStart; - /** The end of the selection in {@link #mFile}. - * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ - private final int mSelectionEnd; - - /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */ - private ICompilationUnit mUnit; - /** The actual string selected, after UTF characters have been escaped, good for display. - * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ - private String mTokenString; - - /** The XML string ID selected by the user in the wizard. */ - private String mXmlStringId; - /** The XML string value. Might be different than the initial selected string. */ - private String mXmlStringValue; - /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user - * in the wizard. */ - private String mTargetXmlFileWsPath; - - /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and - * used by {@link #createChange(IProgressMonitor)}. */ - private ArrayList mChanges; - - private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); - - private static final String KEY_MODE = "mode"; //$NON-NLS-1$ - private static final String KEY_FILE = "file"; //$NON-NLS-1$ - private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$ - private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$ - private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$ - private static final String KEY_TOK_ESC = "tok-esc"; //$NON-NLS-1$ - - public ExtractStringRefactoring(Map arguments) - throws NullPointerException { - mMode = Mode.valueOf(arguments.get(KEY_MODE)); - - IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT)); - mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path); - - if (mMode == Mode.EDIT_SOURCE) { - path = Path.fromPortableString(arguments.get(KEY_FILE)); - mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path); - - mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START)); - mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END)); - mTokenString = arguments.get(KEY_TOK_ESC); - } else { - mFile = null; - mSelectionStart = mSelectionEnd = -1; - mTokenString = null; - } - } - - private Map createArgumentMap() { - HashMap args = new HashMap(); - args.put(KEY_MODE, mMode.name()); - args.put(KEY_PROJECT, mProject.getFullPath().toPortableString()); - if (mMode == Mode.EDIT_SOURCE) { - args.put(KEY_FILE, mFile.getFullPath().toPortableString()); - args.put(KEY_SEL_START, Integer.toString(mSelectionStart)); - args.put(KEY_SEL_END, Integer.toString(mSelectionEnd)); - args.put(KEY_TOK_ESC, mTokenString); - } - return args; - } - - /** - * Constructor to use when the Extract String refactoring is called on an - * *existing* source file. Its purpose is then to get the selected string of - * the source and propose to change it by an XML id. The XML id may be a new one - * or an existing one. - * - * @param file The source file to process. Cannot be null. File must exist in workspace. - * @param selection The selection in the source file. Cannot be null or empty. - */ - public ExtractStringRefactoring(IFile file, ITextSelection selection) { - mMode = Mode.EDIT_SOURCE; - mFile = file; - mProject = file.getProject(); - mSelectionStart = selection.getOffset(); - mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1); - } - - /** - * Constructor to use when the Extract String refactoring is called without - * any source file. Its purpose is then to create a new XML string ID. - * - * @param project The project where the target XML file to modify is located. Cannot be null. - * @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be - * used. - */ - public ExtractStringRefactoring(IProject project, boolean enforceNew) { - mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID; - mFile = null; - mProject = project; - mSelectionStart = mSelectionEnd = -1; - } - - /** - * @see org.eclipse.ltk.core.refactoring.Refactoring#getName() - */ - @Override - public String getName() { - if (mMode == Mode.SELECT_ID) { - return "Create or USe Android String"; - } else if (mMode == Mode.SELECT_NEW_ID) { - return "Create New Android String"; - } - - return "Extract Android String"; - } - - public Mode getMode() { - return mMode; - } - - /** - * Gets the actual string selected, after UTF characters have been escaped, - * good for display. - */ - public String getTokenString() { - return mTokenString; - } - - public String getXmlStringId() { - return mXmlStringId; - } - - /** - * Step 1 of 3 of the refactoring: - * Checks that the current selection meets the initial condition before the ExtractString - * wizard is shown. The check is supposed to be lightweight and quick. Note that at that - * point the wizard has not been created yet. - *

                      - * Here we scan the source buffer to find the token matching the selection. - * The check is successful is a Java string literal is selected, the source is in sync - * and is not read-only. - *

                      - * This is also used to extract the string to be modified, so that we can display it in - * the refactoring wizard. - * - * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor) - * - * @throws CoreException - */ - @Override - public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) - throws CoreException, OperationCanceledException { - - mUnit = null; - mTokenString = null; - - RefactoringStatus status = new RefactoringStatus(); - - try { - monitor.beginTask("Checking preconditions...", 5); - - if (mMode != Mode.EDIT_SOURCE) { - monitor.worked(5); - return status; - } - - if (!checkSourceFile(mFile, status, monitor)) { - return status; - } - - // Try to get a compilation unit from this file. If it fails, mUnit is null. - try { - mUnit = JavaCore.createCompilationUnitFrom(mFile); - - // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar - if (mUnit.isReadOnly()) { - status.addFatalError("The file is read-only, please make it writeable first."); - return status; - } - - // This is a Java file. Check if it contains the selection we want. - if (!findSelectionInJavaUnit(mUnit, status, monitor)) { - return status; - } - - } catch (Exception e) { - // That was not a Java file. Ignore. - } - - if (mUnit == null) { - // Check this an XML file and get the selection and its context. - // TODO - status.addFatalError("Selection must be inside a Java source file."); - } - } finally { - monitor.done(); - } - - return status; - } - - /** - * Try to find the selected Java element in the compilation unit. - * - * If selection matches a string literal, capture it, otherwise add a fatal error - * to the status. - * - * On success, advance the monitor by 3. - */ - private boolean findSelectionInJavaUnit(ICompilationUnit unit, - RefactoringStatus status, IProgressMonitor monitor) { - try { - IBuffer buffer = unit.getBuffer(); - - IScanner scanner = ToolFactory.createScanner( - false, //tokenizeComments - false, //tokenizeWhiteSpace - false, //assertMode - false //recordLineSeparator - ); - scanner.setSource(buffer.getCharacters()); - monitor.worked(1); - - for(int token = scanner.getNextToken(); - token != ITerminalSymbols.TokenNameEOF; - token = scanner.getNextToken()) { - if (scanner.getCurrentTokenStartPosition() <= mSelectionStart && - scanner.getCurrentTokenEndPosition() >= mSelectionEnd) { - // found the token, but only keep of the right type - if (token == ITerminalSymbols.TokenNameStringLiteral) { - mTokenString = new String(scanner.getCurrentTokenSource()); - } - break; - } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) { - // scanner is past the selection, abort. - break; - } - } - } catch (JavaModelException e1) { - // Error in unit.getBuffer. Ignore. - } catch (InvalidInputException e2) { - // Error in scanner.getNextToken. Ignore. - } finally { - monitor.worked(1); - } - - if (mTokenString != null) { - // As a literal string, the token should have surrounding quotes. Remove them. - int len = mTokenString.length(); - if (len > 0 && - mTokenString.charAt(0) == '"' && - mTokenString.charAt(len - 1) == '"') { - mTokenString = mTokenString.substring(1, len - 1); - } - // We need a non-empty string literal - if (mTokenString.length() == 0) { - mTokenString = null; - } - } - - if (mTokenString == null) { - status.addFatalError("Please select a Java string literal."); - } - - monitor.worked(1); - return status.isOK(); - } - - /** - * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit() - * Might not be useful. - * - * On success, advance the monitor by 2. - * - * @return False if caller should abort, true if caller should continue. - */ - private boolean checkSourceFile(IFile file, - RefactoringStatus status, - IProgressMonitor monitor) { - // check whether the source file is in sync - if (!file.isSynchronized(IResource.DEPTH_ZERO)) { - status.addFatalError("The file is not synchronized. Please save it first."); - return false; - } - monitor.worked(1); - - // make sure we can write to it. - ResourceAttributes resAttr = file.getResourceAttributes(); - if (resAttr == null || resAttr.isReadOnly()) { - status.addFatalError("The file is read-only, please make it writeable first."); - return false; - } - monitor.worked(1); - - return true; - } - - /** - * Step 2 of 3 of the refactoring: - * Check the conditions once the user filled values in the refactoring wizard, - * then prepare the changes to be applied. - *

                      - * In this case, most of the sanity checks are done by the wizard so essentially this - * should only be called if the wizard positively validated the user input. - * - * Here we do check that the target resource XML file either does not exists or - * is not read-only. - * - * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor) - * - * @throws CoreException - */ - @Override - public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) - throws CoreException, OperationCanceledException { - RefactoringStatus status = new RefactoringStatus(); - - try { - monitor.beginTask("Checking post-conditions...", 3); - - if (mXmlStringId == null || mXmlStringId.length() <= 0) { - // this is not supposed to happen - status.addFatalError("Missing replacement string ID"); - } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) { - // this is not supposed to happen - status.addFatalError("Missing target xml file path"); - } - monitor.worked(1); - - // Either that resource must not exist or it must be a writeable file. - IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath); - if (targetXml != null) { - if (targetXml.getType() != IResource.FILE) { - status.addFatalError( - String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath)); - } else { - ResourceAttributes attr = targetXml.getResourceAttributes(); - if (attr != null && attr.isReadOnly()) { - status.addFatalError( - String.format("XML file '%1$s' is read-only.", - mTargetXmlFileWsPath)); - } - } - } - monitor.worked(1); - - if (status.hasError()) { - return status; - } - - mChanges = new ArrayList(); - - - // Prepare the change for the XML file. - - if (!mXmlHelper.isResIdDuplicate(mProject, mTargetXmlFileWsPath, mXmlStringId)) { - // We actually change it only if the ID doesn't exist yet - Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue, - status, SubMonitor.convert(monitor, 1)); - if (change != null) { - mChanges.add(change); - } - } - - if (status.hasError()) { - return status; - } - - if (mMode == Mode.EDIT_SOURCE) { - // Prepare the change to the Java compilation unit - List changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString, - status, SubMonitor.convert(monitor, 1)); - if (changes != null) { - mChanges.addAll(changes); - } - } - - monitor.worked(1); - } finally { - monitor.done(); - } - - return status; - } - - /** - * Internal helper that actually prepares the {@link Change} that adds the given - * ID to the given XML File. - *

                      - * This does not actually modify the file. - * - * @param targetXml The file resource to modify. - * @param xmlStringId The new ID to insert. - * @param tokenString The old string, which will be the value in the XML string. - * @return A new {@link TextEdit} that describes how to change the file. - */ - private Change createXmlChange(IFile targetXml, - String xmlStringId, - String tokenString, - RefactoringStatus status, - SubMonitor subMonitor) { - - TextFileChange xmlChange = new TextFileChange(getName(), targetXml); - xmlChange.setTextType("xml"); //$NON-NLS-1$ - - TextEdit edit = null; - TextEditGroup editGroup = null; - - if (!targetXml.exists()) { - // The XML file does not exist. Simply create it. - StringBuilder content = new StringBuilder(); - content.append("\n"); //$NON-NLS-1$ - content.append("\n"); //$NON-NLS-1$ - content.append(" "). //$NON-NLS-1$ - append(tokenString). - append("\n"); //$NON-NLS-1$ - content.append("\n"); //$NON-NLS-1$ - - edit = new InsertEdit(0, content.toString()); - editGroup = new TextEditGroup("Create in new XML file", edit); - } else { - // The file exist. Attempt to parse it as a valid XML document. - try { - int[] indices = new int[2]; - - // TODO case where we replace the value of an existing XML String ID - - if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$ - // Indices[1] indicates whether we found > or />. It can only be 1 or 2. - // Indices[0] is the position of the first character of either > or />. - // - // Note: we don't even try to adapt our formatting to the existing structure (we - // could by capturing whatever whitespace is after the closing bracket and - // applying it here before our tag, unless we were dealing with an empty - // resource tag.) - - int offset = indices[0]; - int len = indices[1]; - StringBuilder content = new StringBuilder(); - content.append(">\n"); //$NON-NLS-1$ - content.append(" "). //$NON-NLS-1$ - append(tokenString). - append(""); //$NON-NLS-1$ - if (len == 2) { - content.append("\n"); //$NON-NLS-1$ - } - - edit = new ReplaceEdit(offset, len, content.toString()); - editGroup = new TextEditGroup("Insert in XML file", edit); - } - } catch (CoreException e) { - // Failed to read file. Ignore. Will return null below. - } - } - - if (edit == null) { - status.addFatalError(String.format("Failed to modify file %1$s", - mTargetXmlFileWsPath)); - return null; - } - - xmlChange.setEdit(edit); - // The TextEditChangeGroup let the user toggle this change on and off later. - xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup)); - - subMonitor.worked(1); - return xmlChange; - } - - /** - * Parse an XML input stream, looking for an opening tag. - *

                      - * If found, returns the character offest in the buffer of the closing bracket of that - * tag, e.g. the position of > in "". The first character is at offset 0. - *

                      - * The implementation here relies on a simple character-based parser. No DOM nor SAX - * parsing is used, due to the simplified nature of the task: we just want the first - * opening tag, which in our case should be the document root. We deal however with - * with the tag being commented out, so comments are skipped. We assume the XML doc - * is sane, e.g. we don't expect the tag to appear in the middle of a string. But - * again since in fact we want the root element, that's unlikely to happen. - *

                      - * We need to deal with the case where the element is written as , in - * which case the caller will want to replace /> by ">...". To do that we return - * two values: the first offset of the closing tag (e.g. / or >) and the length, which - * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >. - * - * @param contents An existing buffer to parse. - * @param tag The tag to look for. - * @param indices The return values: [0] is the offset of the closing bracket and [1] is - * the length which can be only 1 for > and 2 for /> - * @return True if we found the tag, in which case indices can be used. - */ - private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) { - - BufferedReader br = new BufferedReader(new InputStreamReader(contents)); - StringBuilder sb = new StringBuilder(); // scratch area - - tag = "<" + tag; - int tagLen = tag.length(); - int maxLen = tagLen < 3 ? 3 : tagLen; - - try { - int offset = 0; - int i = 0; - char searching = '<'; // we want opening tags - boolean capture = false; - boolean inComment = false; - boolean inTag = false; - while ((i = br.read()) != -1) { - char c = (char) i; - if (c == searching) { - capture = true; - } - if (capture) { - sb.append(c); - int len = sb.length(); - if (inComment && c == '>') { - // is the comment being closed? - if (len >= 3 && sb.substring(len-3).equals("-->")) { //$NON-NLS-1$ - // yes, comment is closing, stop capturing - capture = false; - inComment = false; - sb.setLength(0); - } - } else if (inTag && c == '>') { - // we're capturing in our tag, waiting for the closing >, we just got it - // so we're totally done here. Simply detect whether it's /> or >. - indices[0] = offset; - indices[1] = 1; - if (sb.charAt(len - 2) == '/') { - indices[0]--; - indices[1]++; - } - return true; - - } else if (!inComment && !inTag) { - // not a comment and not our tag yet, so we're capturing because a - // tag is being opened but we don't know which one yet. - - // look for either the opening or a comment or - // the opening of our tag. - if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$ - inComment = true; - } else if (len == tagLen && sb.toString().equals(tag)) { - inTag = true; - } - - // if we're not interested in this tag yet, deal with when to stop - // capturing: the opening tag ends with either any kind of whitespace - // or with a > or maybe there's a PI that starts with ' || c == '?' || c == ' ' || c == '\n' || c == '\r') { - // stop capturing - capture = false; - sb.setLength(0); - } - } - } - - if (capture && len > maxLen) { - // in any case we don't need to capture more than the size of our tag - // or the comment opening tag - sb.deleteCharAt(0); - } - } - offset++; - } - } catch (IOException e) { - // Ignore. - } finally { - try { - br.close(); - } catch (IOException e) { - // oh come on... - } - } - - return false; - } - - /** - * Computes the changes to be made to Java file(s) and returns a list of {@link Change}. - */ - private List computeJavaChanges(ICompilationUnit unit, - String xmlStringId, - String tokenString, - RefactoringStatus status, - SubMonitor subMonitor) { - - // Get the Android package name from the Android Manifest. We need it to create - // the FQCN of the R class. - String packageName = null; - String error = null; - IResource manifestFile = mProject.findMember(AndroidConstants.FN_ANDROID_MANIFEST); - if (manifestFile == null || manifestFile.getType() != IResource.FILE) { - error = "File not found"; - } else { - try { - AndroidManifestParser manifest = AndroidManifestParser.parseForData( - (IFile) manifestFile); - if (manifest == null) { - error = "Invalid content"; - } else { - packageName = manifest.getPackage(); - if (packageName == null) { - error = "Missing package definition"; - } - } - } catch (CoreException e) { - error = e.getLocalizedMessage(); - } - } - - if (error != null) { - status.addFatalError( - String.format("Failed to parse file %1$s: %2$s.", - manifestFile.getFullPath(), error)); - return null; - } - - // TODO in a future version we might want to collect various Java files that - // need to be updated in the same project and process them all together. - // To do that we need to use an ASTRequestor and parser.createASTs, kind of - // like this: - // - // ASTRequestor requestor = new ASTRequestor() { - // @Override - // public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) { - // super.acceptAST(sourceUnit, astNode); - // // TODO process astNode - // } - // }; - // ... - // parser.createASTs(compilationUnits, bindingKeys, requestor, monitor) - // - // and then add multiple TextFileChange to the changes arraylist. - - // Right now the changes array will contain one TextFileChange at most. - ArrayList changes = new ArrayList(); - - // This is the unit that will be modified. - TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource()); - change.setTextType("java"); //$NON-NLS-1$ - - // Create an AST for this compilation unit - ASTParser parser = ASTParser.newParser(AST.JLS3); - parser.setProject(unit.getJavaProject()); - parser.setSource(unit); - parser.setResolveBindings(true); - ASTNode node = parser.createAST(subMonitor.newChild(1)); - - // The ASTNode must be a CompilationUnit, by design - if (!(node instanceof CompilationUnit)) { - status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$ - node.getClass())); - return null; - } - - // ImportRewrite will allow us to add the new type to the imports and will resolve - // what the Java source must reference, e.g. the FQCN or just the simple name. - ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true); - String Rqualifier = packageName + ".R"; //$NON-NLS-1$ - Rqualifier = importRewrite.addImport(Rqualifier); - - // Rewrite the AST itself via an ASTVisitor - AST ast = node.getAST(); - ASTRewrite astRewrite = ASTRewrite.create(ast); - ArrayList astEditGroups = new ArrayList(); - ReplaceStringsVisitor visitor = new ReplaceStringsVisitor( - ast, astRewrite, astEditGroups, - tokenString, Rqualifier, xmlStringId); - node.accept(visitor); - - // Finally prepare the change set - try { - MultiTextEdit edit = new MultiTextEdit(); - - // Create the edit to change the imports, only if anything changed - TextEdit subEdit = importRewrite.rewriteImports(subMonitor.newChild(1)); - if (subEdit.hasChildren()) { - edit.addChild(subEdit); - } - - // Create the edit to change the Java source, only if anything changed - subEdit = astRewrite.rewriteAST(); - if (subEdit.hasChildren()) { - edit.addChild(subEdit); - } - - // Only create a change set if any edit was collected - if (edit.hasChildren()) { - change.setEdit(edit); - - // Create TextEditChangeGroups which let the user turn changes on or off - // individually. This must be done after the change.setEdit() call above. - for (TextEditGroup editGroup : astEditGroups) { - change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup)); - } - - changes.add(change); - } - - // TODO to modify another Java source, loop back to the creation of the - // TextFileChange and accumulate in changes. Right now only one source is - // modified. - - if (changes.size() > 0) { - return changes; - } - - subMonitor.worked(1); - - } catch (CoreException e) { - // ImportRewrite.rewriteImports failed. - status.addFatalError(e.getMessage()); - } - return null; - } - - public class ReplaceStringsVisitor extends ASTVisitor { - - private final AST mAst; - private final ASTRewrite mRewriter; - private final String mOldString; - private final String mRQualifier; - private final String mXmlId; - private final ArrayList mEditGroups; - - public ReplaceStringsVisitor(AST ast, - ASTRewrite astRewrite, - ArrayList editGroups, - String oldString, - String rQualifier, - String xmlId) { - mAst = ast; - mRewriter = astRewrite; - mEditGroups = editGroups; - mOldString = oldString; - mRQualifier = rQualifier; - mXmlId = xmlId; - } - - @Override - public boolean visit(StringLiteral node) { - if (node.getLiteralValue().equals(mOldString)) { - - Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ - SimpleName idName = mAst.newSimpleName(mXmlId); - QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName); - - TextEditGroup editGroup = new TextEditGroup("Replace string by ID"); - mEditGroups.add(editGroup); - mRewriter.replace(node, newNode, editGroup); - } - return super.visit(node); - } - } - - /** - * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the - * work and creates a descriptor that can be used to replay that refactoring later. - * - * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor) - * - * @throws CoreException - */ - @Override - public Change createChange(IProgressMonitor monitor) - throws CoreException, OperationCanceledException { - - try { - monitor.beginTask("Applying changes...", 1); - - CompositeChange change = new CompositeChange( - getName(), - mChanges.toArray(new Change[mChanges.size()])) { - @Override - public ChangeDescriptor getDescriptor() { - - String comment = String.format( - "Extracts string '%1$s' into R.string.%2$s", - mTokenString, - mXmlStringId); - - ExtractStringDescriptor desc = new ExtractStringDescriptor( - mProject.getName(), //project - comment, //description - comment, //comment - createArgumentMap()); - - return new RefactoringChangeDescriptor(desc); - } - }; - - monitor.worked(1); - - return change; - - } finally { - monitor.done(); - } - - } - - /** - * Given a file project path, returns its resource in the same project than the - * compilation unit. The resource may not exist. - */ - private IResource getTargetXmlResource(String xmlFileWsPath) { - IResource resource = mProject.getFile(xmlFileWsPath); - return resource; - } - - /** - * Sets the replacement string ID. Used by the wizard to set the user input. - */ - public void setNewStringId(String newStringId) { - mXmlStringId = newStringId; - } - - /** - * Sets the replacement string ID. Used by the wizard to set the user input. - */ - public void setNewStringValue(String newStringValue) { - mXmlStringValue = newStringValue; - } - - /** - * Sets the target file. This is a project path, e.g. "/res/values/strings.xml". - * Used by the wizard to set the user input. - */ - public void setTargetFile(String targetXmlFileWsPath) { - mTargetXmlFileWsPath = targetXmlFileWsPath; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java deleted file mode 100644 index cfcc546..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import org.eclipse.core.resources.IProject; -import org.eclipse.ltk.ui.refactoring.RefactoringWizard; - -/** - * A wizard for ExtractString based on a simple dialog with one page. - * - * @see ExtractStringInputPage - * @see ExtractStringRefactoring - */ -public class ExtractStringWizard extends RefactoringWizard { - - private final IProject mProject; - - /** - * Create a wizard for ExtractString based on a simple dialog with one page. - * - * @param ref The instance of {@link ExtractStringRefactoring} to associate to the wizard. - * @param project The project where the wizard was invoked from (e.g. where the user selection - * happened, so that we can retrieve project resources.) - */ - public ExtractStringWizard(ExtractStringRefactoring ref, IProject project) { - super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE); - mProject = project; - setDefaultPageTitle(ref.getName()); - } - - @Override - protected void addUserInputPages() { - addPage(new ExtractStringInputPage(mProject)); - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/XmlStringFileHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/XmlStringFileHelper.java deleted file mode 100644 index 6c8bbdb..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/XmlStringFileHelper.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.refactorings.extractstring; - -import com.android.ide.eclipse.common.project.AndroidXPathFactory; - -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.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import java.util.HashMap; -import java.util.HashSet; - -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; - -/** - * - */ -class XmlStringFileHelper { - - /** A temporary cache of R.string IDs defined by a given xml file. The key is the - * project path of the file, the data is a set of known string Ids for that file. */ - private HashMap> mResIdCache; - /** An instance of XPath, created lazily on demand. */ - private XPath mXPath; - - public XmlStringFileHelper() { - } - - /** - * Utility method used by the wizard to check whether the given string ID is already - * defined in the XML file which path is given. - * - * @param project The project contain the XML file. - * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml". - * The given file may or may not exist. - * @param stringId The string ID to find. - * @return True if such a string ID is already defined. - */ - public boolean isResIdDuplicate(IProject project, String xmlFileWsPath, String stringId) { - // This is going to be called many times on the same file. - // Build a cache of the existing IDs for a given file. - if (mResIdCache == null) { - mResIdCache = new HashMap>(); - } - HashSet cache = mResIdCache.get(xmlFileWsPath); - if (cache == null) { - cache = getResIdsForFile(project, xmlFileWsPath); - mResIdCache.put(xmlFileWsPath, cache); - } - - return cache.contains(stringId); - } - - /** - * Extract all the defined string IDs from a given file using XPath. - * @param project The project contain the XML file. - * @param xmlFileWsPath The project path of the file to parse. It may not exist. - * @return The set of all string IDs defined in the file. The returned set is always non - * null. It is empty if the file does not exist. - */ - private HashSet getResIdsForFile(IProject project, String xmlFileWsPath) { - HashSet ids = new HashSet(); - - if (mXPath == null) { - mXPath = AndroidXPathFactory.newXPath(); - } - - // Access the project that contains the resource that contains the compilation unit - IResource resource = project.getFile(xmlFileWsPath); - - if (resource != null && resource.exists() && resource.getType() == IResource.FILE) { - InputSource source; - try { - source = new InputSource(((IFile) resource).getContents()); - - // We want all the IDs in an XML structure like this: - // - // something - // - - String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$ - - Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET); - if (result instanceof NodeList) { - NodeList list = (NodeList) result; - for (int n = list.getLength() - 1; n >= 0; n--) { - String id = list.item(n).getNodeValue(); - ids.add(id); - } - } - - } catch (CoreException e1) { - // IFile.getContents failed. Ignore. - } catch (XPathExpressionException e) { - // mXPath.evaluate failed. Ignore. - } - } - - return ids; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java index 03ebf3e..09afab3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java @@ -17,7 +17,7 @@ package com.android.ide.eclipse.adt.sdk; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer; import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java index 966c5c8..9618661 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java @@ -17,8 +17,8 @@ package com.android.ide.eclipse.adt.ui; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringRefactoring; -import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringWizard; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; import com.android.ide.eclipse.common.resources.IResourceRepository; import com.android.ide.eclipse.common.resources.ResourceItem; import com.android.ide.eclipse.common.resources.ResourceType; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ResourceChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ResourceChooser.java index d7eeb04..f42839b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ResourceChooser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ResourceChooser.java @@ -16,8 +16,8 @@ package com.android.ide.eclipse.adt.ui; -import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringRefactoring; -import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringWizard; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; import com.android.ide.eclipse.common.resources.IResourceRepository; import com.android.ide.eclipse.common.resources.ResourceItem; import com.android.ide.eclipse.common.resources.ResourceType; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportAction.java new file mode 100644 index 0000000..63a9b24 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportAction.java @@ -0,0 +1,65 @@ +/* + * 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.wizards.actions; + +import com.android.ide.eclipse.common.project.ExportHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +public class ExportAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + 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) { + ExportHelper.exportProject(project); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportWizardAction.java new file mode 100644 index 0000000..f5a25b0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/ExportWizardAction.java @@ -0,0 +1,57 @@ +/* + * 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.wizards.actions; + +import com.android.ide.eclipse.adt.wizards.export.ExportWizard; + +import org.eclipse.jface.action.IAction; +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) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + + // 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(); + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ExportWizard.java new file mode 100644 index 0000000..ae91549 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ExportWizard.java @@ -0,0 +1,570 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.jarutils.KeystoreHelper; +import com.android.jarutils.SignedJarBuilder; +import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; +import com.android.jarutils.DebugKeyProvider.KeytoolException; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +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.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * 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_large.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 String PROPERTY_FILENAME = "baseFilename"; //$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() { + 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 mDestinationParentFolder; + + private ExportWizardPage mKeystoreSelectionPage; + private ExportWizardPage mKeyCreationPage; + private ExportWizardPage mKeySelectionPage; + private ExportWizardPage mKeyCheckPage; + + private boolean mKeyCreationMode; + + private List mExistingAliases; + + private Map mApkMap; + + 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, + mDestinationParentFolder.getAbsolutePath()); + ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME, + mApkMap.get(null)[APK_FILE_DEST]); + + // 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 + */ + 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 { + // first we make sure the project is built + mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor); + + // if needed, create the keystore and/or key. + if (mKeystoreCreationMode || mKeyCreationMode) { + final ArrayList output = new ArrayList(); + boolean createdStore = KeystoreHelper.createNewStore( + mKeystore, + null /*storeType*/, + mKeystorePassword, + mKeyAlias, + mKeyPassword, + mDName, + mValidity, + new IKeyGenOutput() { + public void err(String message) { + output.add(message); + } + 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(); + } 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) { + // get the output folder of the project to export. + // this is where we'll find the built apks to resign and export. + IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject); + if (outputIFolder == null) { + return false; + } + String outputOsPath = outputIFolder.getLocation().toOSString(); + + // now generate the packages. + Set> set = mApkMap.entrySet(); + for (Entry entry : set) { + String[] defaultApk = entry.getValue(); + String srcFilename = defaultApk[APK_FILE_SOURCE]; + String destFilename = defaultApk[APK_FILE_DEST]; + + FileOutputStream fos = new FileOutputStream( + new File(mDestinationParentFolder, destFilename)); + SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); + + // get the input file. + FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename)); + + // add the content of the source file to the output file, and sign it at + // the same time. + try { + builder.writeZip(fis, null /* filter */); + // close the builder: write the final signature files, and close the archive. + builder.close(); + } finally { + try { + fis.close(); + } finally { + fos.close(); + } + } + } + return true; + } + } catch (FileNotFoundException e) { + displayError(e); + } catch (NoSuchAlgorithmException e) { + displayError(e); + } catch (IOException e) { + displayError(e); + } catch (GeneralSecurityException e) { + displayError(e); + } catch (KeytoolException e) { + displayError(e); + } catch (CoreException e) { + displayError(e); + } + + 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 mApkMap != null && mApkMap.size() > 0 && + ((mPrivateKey != null && mCertificate != null) + || mKeystoreCreationMode || mKeyCreationMode) && + mDestinationParentFolder != null; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection) + */ + 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 aliases) { + mExistingAliases = aliases; + } + + List 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; + } + + void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + void setDestination(File parentFolder, Map apkMap) { + mDestinationParentFolder = parentFolder; + mApkMap = apkMap; + } + + void resetDestination() { + mDestinationParentFolder = null; + mApkMap = 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(Exception e) { + String message = getExceptionMessage(e); + displayError(message); + + AdtPlugin.log(e, "Export Wizard Error"); + } + + /** + * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns + * null, the method is called again on the cause of the Throwable object. + *

                      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) { + Throwable cause = t.getCause(); + if (cause != null) { + return getExceptionMessage(cause); + } + + // no more cause and still no message. display the first exception. + return t.getClass().getCanonicalName(); + } + + return message; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCheckPage.java new file mode 100644 index 0000000..0672d7b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCheckPage.java @@ -0,0 +1,443 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.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.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * Final page of the wizard that checks the key and ask for the ouput location. + */ +final class KeyCheckPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private PrivateKey mPrivateKey; + private X509Certificate mCertificate; + private Text mDestination; + private boolean mFatalSigningError; + private FormText mDetailText; + /** The Apk Config map for the current project */ + private Map mApkConfig; + 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 + } + + 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() { + 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(); + mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project); + + String destination = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_DESTINATION); + String filename = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_FILENAME); + if (destination != null && filename != null) { + mDestination.setText(destination + File.separator + filename); + } + } + + // 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("

                      Certificate expires in %d years.

                      ", + validity)); + + if (validity < 25) { + sb.append("

                      Make sure the certificate is valid for the planned lifetime of the product.

                      "); + sb.append("

                      If the certificate expires, you will be forced to sign your application with a different one.

                      "); + sb.append("

                      Applications cannot be upgraded if their certificate changes from one version to another, "); + sb.append("forcing a full uninstall/install, which will make the user lose his/her data.

                      "); + sb.append("

                      Android Market currently requires certificates to be valid until 2033.

                      "); + } + + 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( + "

                      Certificate expired on %s

                      ", + 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( + "

                      Certificate expires on %s.

                      ", + mCertificate.getNotAfter().toString())); + + int expirationYear = expirationCalendar.get(Calendar.YEAR); + int thisYear = today.get(Calendar.YEAR); + + if (thisYear + 25 < expirationYear) { + // do nothing + } else { + if (expirationYear == thisYear) { + sb.append("

                      The certificate expires this year.

                      "); + } else { + int count = expirationYear-thisYear; + sb.append(String.format( + "

                      The Certificate expires in %1$s %2$s.

                      ", + count, count == 1 ? "year" : "years")); + } + + sb.append("

                      Make sure the certificate is valid for the planned lifetime of the product.

                      "); + sb.append("

                      If the certificate expires, you will be forced to sign your application with a different one.

                      "); + sb.append("

                      Applications cannot be upgraded if their certificate changes from one version to another, "); + sb.append("forcing a full uninstall/install, which will make the user lose his/her data.

                      "); + sb.append("

                      Android Market currently requires certificates to be valid until 2033.

                      "); + } + + 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; + } + + // display the list of files that will actually be created + Map apkFileMap = getApkFileMap(file); + + // display them + boolean fileExists = false; + StringBuilder sb = new StringBuilder(String.format( + "

                      This will create the following files:

                      ")); + + Set> set = apkFileMap.entrySet(); + for (Entry entry : set) { + String[] apkArray = entry.getValue(); + String filename = apkArray[ExportWizard.APK_FILE_DEST]; + File f = new File(parentFolder, filename); + if (f.isFile()) { + fileExists = true; + sb.append(String.format("
                    • %1$s (WARNING: already exists)
                    • ", filename)); + } else if (f.isDirectory()) { + setErrorMessage(String.format("%1$s is a directory.", filename)); + // reset canFinish in the wizard. + mWizard.resetDestination(); + setPageComplete(false); + return; + } else { + sb.append(String.format("
                    • %1$s
                    • ", filename)); + } + } + + mDestinationDetails = sb.toString(); + + // no error, set the destination in the wizard. + mWizard.setDestination(parentFolder, apkFileMap); + setPageComplete(true); + + // However, we should also test if the file already exists. + if (fileExists) { + setMessage("A destination file already exists.", WARNING); + } + + 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("
                      "); + if (mKeyDetails != null) { + sb.append(mKeyDetails); + } + + if (mDestinationDetails != null && mFatalSigningError == false) { + sb.append(mDestinationDetails); + } + + sb.append("
                      "); + + mDetailText.setText(sb.toString(), true /* parseTags */, + true /* expandURLs */); + + mDetailText.getParent().layout(); + + updateScrolling(); + + } + + /** + * Creates the list of destination filenames based on the content of the destination field + * and the list of APK configurations for the project. + * + * @param file File name from the destination field + * @return A list of destination filenames based file and the list of APK + * configurations for the project. + */ + private Map getApkFileMap(File file) { + String filename = file.getName(); + + HashMap map = new HashMap(); + + // add the default APK filename + String[] apkArray = new String[ExportWizard.APK_COUNT]; + apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( + mWizard.getProject(), null /*config*/); + apkArray[ExportWizard.APK_FILE_DEST] = filename; + map.put(null, apkArray); + + // add the APKs for each APK configuration. + if (mApkConfig != null && mApkConfig.size() > 0) { + // remove the extension. + int index = filename.lastIndexOf('.'); + String base = filename.substring(0, index); + String extension = filename.substring(index); + + Set> set = mApkConfig.entrySet(); + for (Entry entry : set) { + apkArray = new String[ExportWizard.APK_COUNT]; + apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( + mWizard.getProject(), entry.getKey()); + apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension; + map.put(entry.getKey(), apkArray); + } + } + + return map; + } + + @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/wizards/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCreationPage.java new file mode 100644 index 0000000..80854d3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeyCreationPage.java @@ -0,0 +1,332 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.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 mExistingAliases; + + + protected KeyCreationPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Key Creation"); + setDescription(""); // TODO? + } + + 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() { + 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() { + public void modifyText(ModifyEvent e) { + mWizard.setKeyAlias(mAlias.getText().trim()); + onChange(); + } + }); + mKeyPassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeyPassword(mKeyPassword.getText()); + onChange(); + } + }); + mKeyPassword2.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onChange(); + } + }); + + validityText.addModifyListener(new ModifyListener() { + 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() { + 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/wizards/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeySelectionPage.java new file mode 100644 index 0000000..57637ea --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeySelectionPage.java @@ -0,0 +1,266 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.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 + } + + 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() { + 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 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 aliasList = new ArrayList(); + + 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/wizards/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeystoreSelectionPage.java new file mode 100644 index 0000000..07990e7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/KeystoreSelectionPage.java @@ -0,0 +1,260 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.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 + } + + 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() { + public void modifyText(ModifyEvent e) { + mWizard.setKeystore(mKeystore.getText().trim()); + onChange(); + } + }); + + mKeystorePassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeystorePassword(mKeystorePassword.getText()); + onChange(); + } + }); + + mKeystorePassword2.addModifyListener(new ModifyListener() { + 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/wizards/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ProjectCheckPage.java new file mode 100644 index 0000000..b462a0d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/export/ProjectCheckPage.java @@ -0,0 +1,302 @@ +/* + * 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.wizards.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidConstants; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.wizards.export.ExportWizard.ExportWizardPage; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.ProjectChooserHelper; + +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * 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 Display mDisplay; + 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."); + } + + public void createControl(Composite parent) { + mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); + mDisplay = parent.getDisplay(); + + 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() { + 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()); + + 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(AndroidConstants.NATURE) == 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.getOutputFolder(project); + if (outputIFolder != null) { + String outputOsPath = outputIFolder.getLocation().toOSString(); + String apkFilePath = outputOsPath + File.separator + project.getName() + + AndroidConstants.DOT_ANDROID_PACKAGE; + + File f = new File(apkFilePath); + if (f.isFile() == false) { + addError(mErrorComposite, + String.format("%1$s/%2$s/%1$s%3$s does not exists!", + project.getName(), + outputIFolder.getName(), + AndroidConstants.DOT_ANDROID_PACKAGE)); + } + } else { + addError(mErrorComposite, + "Unable to get the output folder of the project!"); + } + + + // project is an android project, we check the debuggable attribute. + AndroidManifestParser manifestParser = AndroidManifestParser.parse( + BaseProjectHelper.getJavaProject(project), null /* errorListener */, + true /* gatherData */, false /* markErrors */); + + Boolean debuggable = manifestParser.getDebuggable(); + + if (debuggable != null && debuggable == Boolean.TRUE) { + addWarning(mErrorComposite, + "The manifest 'debuggable' attribute is set to true.\nYou should set it to false for applications that you release to the public."); + } + + // 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 = AdtPlugin.getImageLoader().loadImage(IMG_ERROR, mDisplay); + } + + 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 = AdtPlugin.getImageLoader().loadImage(IMG_WARNING, mDisplay); + } + + 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/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java index 8b315d4..020895b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java @@ -18,8 +18,8 @@ package com.android.ide.eclipse.adt.wizards.newproject; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; -import com.android.ide.eclipse.adt.project.AndroidNature; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.AndroidNature; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.adt.wizards.newproject.NewProjectCreationPage.IMainInfo; import com.android.ide.eclipse.adt.wizards.newproject.NewTestProjectCreationPage.TestInfo; diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java index 98817c6..54fcb99 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java @@ -15,7 +15,7 @@ */ package com.android.ide.eclipse.tests.functests.sampleProjects; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.wizards.newproject.StubSampleProjectWizard; import com.android.ide.eclipse.tests.FuncTestCase; diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java deleted file mode 100644 index 0860e40..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.build; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import junit.framework.TestCase; - -public class BaseBuilderTest extends TestCase { - - public void testParseAaptOutput() { - Pattern p = Pattern.compile( "^(.+):(\\d+):\\s(.+)$"); //$NON-NLS-1$ - String s = "C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml:11: WARNING: empty 'some warning text"; - - Matcher m = p.matcher(s); - assertEquals(true, m.matches()); - assertEquals("C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml", m.group(1)); - assertEquals("11", m.group(2)); - assertEquals("WARNING: empty 'some warning text", m.group(3)); - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/build/BaseBuilderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/build/BaseBuilderTest.java new file mode 100644 index 0000000..a1d658b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/build/BaseBuilderTest.java @@ -0,0 +1,37 @@ +/* + * 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.build; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import junit.framework.TestCase; + +public class BaseBuilderTest extends TestCase { + + public void testParseAaptOutput() { + Pattern p = Pattern.compile( "^(.+):(\\d+):\\s(.+)$"); //$NON-NLS-1$ + String s = "C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml:11: WARNING: empty 'some warning text"; + + Matcher m = p.matcher(s); + assertEquals(true, m.matches()); + assertEquals("C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml", m.group(1)); + assertEquals("11", m.group(2)); + assertEquals("WARNING: empty 'some warning text", m.group(3)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/ProjectHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/ProjectHelperTest.java new file mode 100644 index 0000000..6080102 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/project/ProjectHelperTest.java @@ -0,0 +1,68 @@ +/* + * 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.project; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.mock.ClasspathEntryMock; +import com.android.ide.eclipse.mock.JavaProjectMock; + +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.JavaModelException; + +import junit.framework.TestCase; + +public class ProjectHelperTest extends TestCase { + + /** The old container id */ + private final static String OLD_CONTAINER_ID = + "com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer"; //$NON-NLS-1$ + + /** The container id for the android framework jar file */ + private final static String CONTAINER_ID = + "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ + + @Override + public void setUp() throws Exception { + // pass for now + } + + @Override + public void tearDown() throws Exception { + // pass for now + } + + public final void testFixProjectClasspathEntriesFromOldContainer() throws JavaModelException { + // create a project with a path to an android .zip + JavaProjectMock javaProject = new JavaProjectMock( + new IClasspathEntry[] { + new ClasspathEntryMock(new Path("Project/src"), //$NON-NLS-1$ + IClasspathEntry.CPE_SOURCE), + new ClasspathEntryMock(new Path(OLD_CONTAINER_ID), + IClasspathEntry.CPE_CONTAINER), + }, + new Path("Project/bin")); + + ProjectHelper.fixProjectClasspathEntries(javaProject); + + IClasspathEntry[] fixedEntries = javaProject.getRawClasspath(); + assertEquals(3, fixedEntries.length); + assertEquals("Project/src", fixedEntries[0].getPath().toString()); + assertEquals(OLD_CONTAINER_ID, fixedEntries[1].getPath().toString()); + assertEquals(CONTAINER_ID, fixedEntries[2].getPath().toString()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java deleted file mode 100644 index 8c52d81..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.project; - -import com.android.ide.eclipse.mock.ClasspathEntryMock; -import com.android.ide.eclipse.mock.JavaProjectMock; - -import org.eclipse.core.runtime.Path; -import org.eclipse.jdt.core.IClasspathEntry; -import org.eclipse.jdt.core.JavaModelException; - -import junit.framework.TestCase; - -public class ProjectHelperTest extends TestCase { - - /** The old container id */ - private final static String OLD_CONTAINER_ID = - "com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer"; //$NON-NLS-1$ - - /** The container id for the android framework jar file */ - private final static String CONTAINER_ID = - "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ - - @Override - public void setUp() throws Exception { - // pass for now - } - - @Override - public void tearDown() throws Exception { - // pass for now - } - - public final void testFixProjectClasspathEntriesFromOldContainer() throws JavaModelException { - // create a project with a path to an android .zip - JavaProjectMock javaProject = new JavaProjectMock( - new IClasspathEntry[] { - new ClasspathEntryMock(new Path("Project/src"), //$NON-NLS-1$ - IClasspathEntry.CPE_SOURCE), - new ClasspathEntryMock(new Path(OLD_CONTAINER_ID), - IClasspathEntry.CPE_CONTAINER), - }, - new Path("Project/bin")); - - ProjectHelper.fixProjectClasspathEntries(javaProject); - - IClasspathEntry[] fixedEntries = javaProject.getRawClasspath(); - assertEquals(3, fixedEntries.length); - assertEquals("Project/src", fixedEntries[0].getPath().toString()); - assertEquals(OLD_CONTAINER_ID, fixedEntries[1].getPath().toString()); - assertEquals(CONTAINER_ID, fixedEntries[2].getPath().toString()); - } -} -- cgit v1.1