auto import from //branches/cupcake_rel/...@141571

This commit is contained in:
The Android Open Source Project
2009-03-19 23:08:36 -07:00
parent 6ffae015b4
commit b0bdccc71a
36 changed files with 2967 additions and 198 deletions

View File

@@ -41,7 +41,9 @@ Require-Bundle: com.android.ide.eclipse.ddms,
org.eclipse.wst.xml.core,
org.eclipse.wst.xml.ui,
org.eclipse.jdt.junit,
org.eclipse.jdt.junit.runtime
org.eclipse.jdt.junit.runtime,
org.eclipse.ltk.core.refactoring,
org.eclipse.ltk.ui.refactoring
Eclipse-LazyStart: true
Export-Package: com.android.ide.eclipse.adt,
com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests",

View File

@@ -470,7 +470,7 @@
point="org.eclipse.ui.actionSets">
<actionSet
description="Android Wizards"
id="adt.actionSet1"
id="adt.actionSet.wizards"
label="Android Wizards"
visible="true">
<action
@@ -481,12 +481,6 @@
style="push"
toolbarPath="android_project"
tooltip="Opens a wizard to help create a new Android XML file">
<enablement>
<objectState
name="projectNature"
value="com.android.ide.eclipse.adt.AndroidNature">
</objectState>
</enablement>
</action>
<action
class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
@@ -498,6 +492,21 @@
tooltip="Opens a wizard to help create a new Android project">
</action>
</actionSet>
<actionSet
description="Refactorings for Android"
id="adt.actionSet.refactorings"
label="Android Refactorings"
visible="true">
<action
class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringAction"
definitionId="com.android.ide.eclipse.adt.refactoring.extract.string"
id="com.android.ide.eclipse.adt.actions.ExtractString"
label="Extract Android String"
menubarPath="org.eclipse.jdt.ui.refactoring.menu/codingGroup"
style="push"
tooltip="Extracts a string into Android resource string">
</action>
</actionSet>
</extension>
<extension
point="org.eclipse.debug.core.launchDelegates">
@@ -506,7 +515,7 @@
delegateDescription="Removes the Android JAR from the Bootstrap Classpath"
id="com.android.ide.eclipse.adt.launch.JUnitLaunchConfigDelegate.launchAndroidJunit"
modes="run,debug"
name="Android JUnit"
name="Android JUnit Test"
type="org.eclipse.jdt.junit.launchconfig">
</launchDelegate>
</extension>
@@ -516,7 +525,7 @@
delegate="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchConfigDelegate"
id="com.android.ide.eclipse.adt.junit.launchConfigurationType"
modes="run,debug"
name="Android Instrumentation"
name="Android JUnit Test"
public="true"
sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"
sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer">
@@ -534,7 +543,7 @@
point="org.eclipse.debug.ui.launchConfigurationTabGroups">
<launchConfigurationTabGroup
class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitTabGroup"
description="Android Instrumentation"
description="Android JUnit Test"
id="com.android.ide.eclipse.adt.junit.AndroidJUnitLaunchConfigTabGroup"
type="com.android.ide.eclipse.adt.junit.launchConfigurationType"/>
</extension>
@@ -544,7 +553,7 @@
class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchShortcut"
icon="icons/android.png"
id="com.android.ide.eclipse.adt.junit.launchShortcut"
label="Android Instrumentation"
label="Android JUnit Test"
modes="run,debug">
<contextualLaunch>
<enablement>
@@ -565,4 +574,25 @@
</configurationType>
</shortcut>
</extension>
<extension
point="org.eclipse.ui.commands">
<category
description="Refactorings for Android Projects"
id="com.android.ide.eclipse.adt.refactoring.category"
name="Android Refactorings">
</category>
<command
categoryId="com.android.ide.eclipse.adt.refactoring.category"
description="Extract Strings into Android String Resources"
id="com.android.ide.eclipse.adt.refactoring.extract.string"
name="Extract Android String">
</command>
</extension>
<extension
point="org.eclipse.ltk.core.refactoring.refactoringContributions">
<contribution
class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringContribution"
id="com.android.ide.eclipse.adt.refactoring.extract.string">
</contribution>
</extension>
</plugin>

View File

@@ -1282,7 +1282,7 @@ public class AdtPlugin extends AbstractUIPlugin {
AdtPlugin.PLUGIN_ID,
UNKNOWN_EDITOR);
try {
file.setPersistentProperty(qname, "1");
file.setPersistentProperty(qname, "1"); //$NON-NLS-1$
} catch (CoreException e) {
// pass
}

View File

@@ -196,7 +196,7 @@ public class ApkBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {

View File

@@ -197,7 +197,7 @@ public class PreCompilerBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {

View File

@@ -55,7 +55,7 @@ public class ResourceManagerBuilder extends BaseBuilder {
}
// build() returns a list of project from which this project depends for future compilation.
@SuppressWarnings("unchecked") //$NON-NLS-1$
@SuppressWarnings("unchecked")
@Override
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {

View File

@@ -32,8 +32,30 @@ public class AndroidLaunchConfiguration {
*/
public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
public static final boolean AUTO_TARGET_MODE = true;
public enum TargetMode {
AUTO(true), MANUAL(false);
private boolean mValue;
TargetMode(boolean value) {
mValue = value;
}
public boolean getValue() {
return mValue;
}
public static TargetMode getMode(boolean value) {
for (TargetMode mode : values()) {
if (mode.mValue == value) {
return mode;
}
}
return null;
}
}
/**
* Target selection mode.
* <ul>
@@ -41,7 +63,7 @@ public class AndroidLaunchConfiguration {
* <li><code>false</code>: manual mode</li>
* </ul>
*/
public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
public TargetMode mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
/**
* Indicates whether the emulator should be called with -wipe-data
@@ -81,8 +103,9 @@ public class AndroidLaunchConfiguration {
}
try {
mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
mTargetMode);
boolean value = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
mTargetMode.getValue());
mTargetMode = TargetMode.getMode(value);
} catch (CoreException e) {
// nothing to be done here, we'll use the default value
}

View File

@@ -29,11 +29,15 @@ import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.SyncService.SyncResult;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo.InstallRetryMode;
import com.android.ide.eclipse.adt.launch.DeviceChooserDialog.DeviceChooserResponse;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.avd.AvdManager;
@@ -52,6 +56,9 @@ import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMConnector;
import org.eclipse.jdt.launching.JavaRuntime;
@@ -64,6 +71,7 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -236,7 +244,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
// set default target mode
wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
LaunchConfigDelegate.DEFAULT_TARGET_MODE);
LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());
// default AVD: None
wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);
@@ -332,6 +340,16 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
Sdk currentSdk = Sdk.getCurrent();
AvdManager avdManager = currentSdk.getAvdManager();
// reload the AVDs to make sure we are up to date
try {
avdManager.reloadAvds();
} catch (AndroidLocationException e1) {
// this happens if the AVD Manager failed to find the folder in which the AVDs are
// stored. This is unlikely to happen, but if it does, we should force to go manual
// to allow using physical devices.
config.mTargetMode = TargetMode.MANUAL;
}
// get the project target
final IAndroidTarget projectTarget = currentSdk.getTarget(project);
@@ -356,7 +374,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
* If == 1, launch the application on this AVD/device.
*/
if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) {
if (config.mTargetMode == TargetMode.AUTO) {
// if we are in automatic target mode, we need to find the current devices
IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
@@ -795,6 +813,14 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
return false;
}
// The app is now installed, now try the dependent projects
for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
String msg = String.format("Project dependency found, syncing: %s",
dependentLaunchInfo.getProject().getName());
AdtPlugin.printToConsole(launchInfo.getProject(), msg);
syncApp(dependentLaunchInfo, device);
}
return installResult;
}
@@ -806,6 +832,81 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
return false;
}
/**
* For the current launchInfo, create additional DelayedLaunchInfo that should be used to
* sync APKs that we are dependent on to the device.
*
* @param launchInfo the original launch info that we want to find the
* @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
*/
public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
// Convert to equivalent JavaProject
IJavaProject javaProject;
try {
//assuming this is an Android (and Java) project since it is attached to the launchInfo.
javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
} catch (CoreException e) {
// return empty dependencies
AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
return dependencies;
}
// Get all projects that this depends on
List<IJavaProject> androidProjectList;
try {
androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
} catch (JavaModelException e) {
// return empty dependencies
AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
return dependencies;
}
// for each project, parse manifest and create launch information
for (IJavaProject androidProject : androidProjectList) {
// Parse the Manifest to get various required information
// copied from LaunchConfigDelegate
AndroidManifestParser manifestParser;
try {
manifestParser = AndroidManifestParser.parse(
androidProject, null /* errorListener */,
true /* gatherData */, false /* markErrors */);
} catch (CoreException e) {
AdtPlugin.printErrorToConsole(
launchInfo.getProject(),
String.format("Error parsing manifest of %s",
androidProject.getElementName()));
continue;
}
// Get the APK location (can return null)
IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
if (apk == null) {
// getApplicationPackage will have logged an error message
continue;
}
// Create new launchInfo as an hybrid between parent and dependency information
DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
androidProject.getProject(),
manifestParser.getPackage(),
launchInfo.getLaunchAction(),
apk,
manifestParser.getDebuggable(),
manifestParser.getApiLevelRequirement(),
launchInfo.getLaunch(),
launchInfo.getMonitor());
// Add to the list
dependencies.add(delayedLaunchInfo);
}
return dependencies;
}
/**
* Installs the application package that was pushed to a temporary location on the device.
* @param launchInfo The launch information

View File

@@ -17,6 +17,7 @@
package com.android.ide.eclipse.adt.launch;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
import com.android.ide.eclipse.ddms.DdmsPlugin;
@@ -292,14 +293,15 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
public void initializeFrom(ILaunchConfiguration configuration) {
AvdManager avdManager = Sdk.getCurrent().getAvdManager();
boolean value = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic
TargetMode mode = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic
try {
value = configuration.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, value);
mode = TargetMode.getMode(configuration.getAttribute(
LaunchConfigDelegate.ATTR_TARGET_MODE, mode.getValue()));
} catch (CoreException e) {
// let's not do anything here, we'll use the default value
}
mAutoTargetButton.setSelection(value);
mManualTargetButton.setSelection(!value);
mAutoTargetButton.setSelection(mode.getValue());
mManualTargetButton.setSelection(!mode.getValue());
// look for the project name to get its target.
String stringValue = "";
@@ -354,7 +356,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
mPreferredAvdSelector.setSelection(null);
}
value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
boolean value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
try {
value = configuration.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, value);
} catch (CoreException e) {
@@ -440,7 +442,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
*/
public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
LaunchConfigDelegate.DEFAULT_TARGET_MODE);
LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue());
configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
LaunchConfigDelegate.DEFAULT_SPEED);
configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,

View File

@@ -18,15 +18,14 @@ package com.android.ide.eclipse.adt.launch;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.project.ProjectHelper;
import com.android.ide.eclipse.common.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.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
@@ -51,7 +50,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
/** Target mode parameters: true is automatic, false is manual */
public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
public static final boolean DEFAULT_TARGET_MODE = true; //automatic mode
public static final TargetMode DEFAULT_TARGET_MODE = TargetMode.AUTO;
/**
* Launch action:
@@ -152,7 +151,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
AdtPlugin.printToConsole(project, "Android Launch!");
// check if the project is using the proper sdk.
// if that throws an exception, we simply let it propage to the caller.
// if that throws an exception, we simply let it propagate to the caller.
if (checkAndroidProject(project) == false) {
AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
androidLaunch.stopLaunch();
@@ -215,7 +214,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
AndroidLaunchController controller = AndroidLaunchController.getInstance();
// get the application package
IFile applicationPackage = getApplicationPackage(project);
IFile applicationPackage = ProjectHelper.getApplicationPackage(project);
if (applicationPackage == null) {
androidLaunch.stopLaunch();
return;
@@ -387,39 +386,6 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
}
/**
* 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.
*/
private 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;
}
/**
* Returns the name of the activity.
*/

View File

@@ -58,7 +58,7 @@ public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
/**
*
*/
public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab.png";
public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab.png"; //$NON-NLS-1$
protected static final String EMPTY_STRING = ""; //$NON-NLS-1$

View File

@@ -20,7 +20,7 @@ import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo;
import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
import com.android.ide.eclipse.adt.launch.junit.runtime.AndroidJUnitLaunchInfo;
import com.android.ide.eclipse.adt.launch.junit.runtime.RemoteADTTestRunner;
import com.android.ide.eclipse.adt.launch.junit.runtime.RemoteAdtTestRunner;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
@@ -166,7 +166,7 @@ class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
private final VMRunnerConfiguration mRunConfig;
private final ILaunch mLaunch;
private final AndroidJUnitLaunchInfo mJUnitInfo;
private RemoteADTTestRunner mTestRunner = null;
private RemoteAdtTestRunner mTestRunner = null;
private boolean mIsTerminated = false;
TestRunnerProcess(VMRunnerConfiguration runConfig, ILaunch launch,
@@ -256,7 +256,7 @@ class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
*/
@Override
public void run() {
mTestRunner = new RemoteADTTestRunner();
mTestRunner = new RemoteAdtTestRunner();
mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
}
}

View File

@@ -18,10 +18,11 @@ package com.android.ide.eclipse.adt.launch.junit;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.launch.AndroidLaunch;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration;
import com.android.ide.eclipse.adt.launch.AndroidLaunchController;
import com.android.ide.eclipse.adt.launch.IAndroidLaunchAction;
import com.android.ide.eclipse.adt.launch.LaunchConfigDelegate;
import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.ide.eclipse.common.project.BaseProjectHelper;
@@ -31,6 +32,7 @@ import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
@@ -46,6 +48,7 @@ public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
/** Launch config attribute that stores instrumentation runner */
static final String ATTR_INSTR_NAME = AdtPlugin.PLUGIN_ID + ".instrumentation"; //$NON-NLS-1$
static final String INSTRUMENTATION_OK = null;
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
@Override
@@ -87,7 +90,8 @@ public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
* Helper method to return the set of instrumentations for the Android project
*
* @param project the {@link IProject} to get instrumentations for
* @return null if no error occurred parsing instrumentations
* @return null if error occurred parsing instrumentations, otherwise returns array of
* instrumentation class names
*/
static String[] getInstrumentationsForProject(IProject project) {
if (project != null) {
@@ -117,4 +121,56 @@ public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
TestKindRegistry.JUNIT3_TEST_KIND_ID);
}
/**
* Helper method to determine if specified instrumentation can be used as a test runner
*
* @param project the {@link IJavaProject} to validate
* @param instrumentation the instrumentation class name to validate
* @return <code>INSTRUMENTATION_OK</code> if valid, otherwise returns error message
*/
static String validateInstrumentationRunner(IJavaProject project, String instrumentation) {
AndroidManifestParser manifestParser;
try {
manifestParser = AndroidManifestParser.parse(
project, null /* errorListener */,
true /* gatherData */, false /* markErrors */);
// check if this instrumentation is the standard test runner
if (!instrumentation.equals(AndroidConstants.CLASS_INSTRUMENTATION_RUNNER)) {
// check if it extends the standard test runner
String result = BaseProjectHelper.testClassForManifest(project,
instrumentation, AndroidConstants.CLASS_INSTRUMENTATION_RUNNER, true);
if (result != BaseProjectHelper.TEST_CLASS_OK) {
return String.format("The instrumentation runner must be of type %s",
AndroidConstants.CLASS_INSTRUMENTATION_RUNNER);
}
}
if (!hasTestRunnerLibrary(manifestParser)) {
return String.format("%s does not not use the %s library",
project.getProject().getName(), AndroidConstants.LIBRARY_TEST_RUNNER);
}
} catch (CoreException e) {
String err = String.format("Error parsing AndroidManifest for %s",
project.getProject().getName());
AdtPlugin.log(e, err);
return err;
}
return INSTRUMENTATION_OK;
}
/**
* Helper method to determine if given manifest has a <code>AndroidConstants.LIBRARY_TEST_RUNNER
* </code> library reference
*
* @param manifestParser the {@link AndroidManifestParser} to search
* @return true if test runner library found, false otherwise
*/
static boolean hasTestRunnerLibrary(AndroidManifestParser manifestParser) {
for (String lib : manifestParser.getUsesLibraries()) {
if (lib.equals(AndroidConstants.LIBRARY_TEST_RUNNER)) {
return true;
}
}
return false;
}
}

View File

@@ -691,10 +691,18 @@ public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurat
private void validateInstrumentation(IJavaProject javaProject) {
if (mInstrumentations == null || mInstrumentations.length < 1) {
setErrorMessage("Specified project has no defined instrumentations");
return;
}
String instrumentation = getSelectedInstrumentation();
if (instrumentation == null) {
setErrorMessage("Instrumentation not specified");
return;
}
String result = AndroidJUnitLaunchConfigDelegate.validateInstrumentationRunner(
javaProject, instrumentation);
if (result != AndroidJUnitLaunchConfigDelegate.INSTRUMENTATION_OK) {
setErrorMessage(result);
return;
}
}

View File

@@ -16,6 +16,9 @@
package com.android.ide.eclipse.adt.launch.junit;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.common.AndroidConstants;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
@@ -34,7 +37,7 @@ public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
/**
* Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
* first instrumentation found in the AndroidManifest.
* first instrumentation found in the AndroidManifest.
*/
@Override
protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
@@ -43,10 +46,27 @@ public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
IProject project = element.getResource().getProject();
String[] instrumentations =
AndroidJUnitLaunchConfigDelegate.getInstrumentationsForProject(project);
if (instrumentations != null && instrumentations.length > 0) {
// just pick the first runner
config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
instrumentations[0]);
boolean runnerFound = false;
if (instrumentations != null) {
// just pick the first valid runner
for (String instr : instrumentations) {
if (AndroidJUnitLaunchConfigDelegate.validateInstrumentationRunner(
element.getJavaProject(), instr) ==
AndroidJUnitLaunchConfigDelegate.INSTRUMENTATION_OK) {
config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
instr);
runnerFound = true;
break;
}
}
}
if (!runnerFound) {
// TODO: put this in a string properties
String msg = String.format("ERROR: Application does not specify a %s instrumentation or does not declare uses-library %s",
AndroidConstants.CLASS_INSTRUMENTATION_RUNNER,
AndroidConstants.LIBRARY_TEST_RUNNER);
AdtPlugin.printErrorToConsole(project, msg);
}
AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);

View File

@@ -34,7 +34,7 @@ import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
* @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
*/
@SuppressWarnings("restriction")
public class RemoteADTTestRunner extends RemoteTestRunner {
public class RemoteAdtTestRunner extends RemoteTestRunner {
private AndroidJUnitLaunchInfo mLaunchInfo;
private TestExecution mExecution;
@@ -97,6 +97,8 @@ public class RemoteADTTestRunner extends RemoteTestRunner {
// error occurred during test collection.
reportError(collector.getErrorMessage());
// abort here
notifyTestRunEnded(0);
return;
}
notifyTestRunStarted(collector.getTestCaseCount());
collector.sendTrees(this);

View File

@@ -20,8 +20,10 @@ import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
import com.android.ide.eclipse.common.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;
@@ -34,12 +36,14 @@ 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.
@@ -679,4 +683,71 @@ public final class ProjectHelper {
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<IJavaProject> 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<IJavaProject> projectList = new ArrayList<IJavaProject>(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;
}
}

View File

@@ -0,0 +1,161 @@
/*
* 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.IFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.ui.JavaUI;
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;
/*
* 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.
* <p/>
* The intent of the action is to start a refactoring that extracts a source string and
* replaces it by an Android string resource ID.
* <p/>
* Workflow:
* <ul>
* <li> The action is currently located in the Refactoring menu in the main menu.
* <li> TODO: extend the popup refactoring menu in a Java or Android XML file.
* <li> 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.
* <ul> 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.
* <ul> 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.
* <li> The action creates a new {@link ExtractStringRefactoring} and make it run on in a new
* {@link ExtractStringWizard}.
* <ul>
*/
public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
/** Keep track of the current workbench window. */
private IWorkbenchWindow mWindow;
private ITextSelection mSelection;
private ICompilationUnit mUnit;
/**
* 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.
* <p/>
* 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;
mUnit = null;
if (selection instanceof ITextSelection) {
mSelection = (ITextSelection) selection;
if (mSelection.getLength() > 0) {
mUnit = getCompilationUnit();
}
// Keep for debugging purposes
//System.out.println(String.format("-- Selection: %d + %d = %s",
// mSelection.getOffset(),
// mSelection.getLength(),
// mSelection.getText()));
}
action.setEnabled(mSelection != null && mUnit != null);
}
/**
* Create a new instance of our refactoring and a wizard to configure it.
*/
public void run(IAction action) {
if (mSelection != null && mUnit != null) {
ExtractStringRefactoring ref = new ExtractStringRefactoring(mUnit, mSelection);
RefactoringWizard wizard = new ExtractStringWizard(ref, "Extract Android String");
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
} catch (InterruptedException e) {
// Interrupted. Pass.
}
}
}
/**
* Returns the active {@link ICompilationUnit} or null.
*/
private ICompilationUnit getCompilationUnit() {
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 != null) {
ITypeRoot typeRoot = JavaUI.getEditorInputTypeRoot(input);
// The type root can be either a .class or a .java (aka compilation unit).
// We want the compilation unit kind.
if (typeRoot instanceof ICompilationUnit) {
return (ICompilationUnit) typeRoot;
}
}
}
}
}
return null;
}
}

View File

@@ -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.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);
}
}

View File

@@ -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.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<String, String> mArguments;
public ExtractStringDescriptor(String project, String description, String comment,
Map<String, String> arguments) {
super(ID, project, description, comment,
RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags
);
mArguments = arguments;
}
public Map<String, String> 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)}
* <p/>
* 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;
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.jface.wizard.IWizardPage;
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.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
/**
* @see ExtractStringRefactoring
*/
class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
public ExtractStringInputPage() {
super("ExtractStringInputPage"); //$NON-NLS-1$
}
private Label mStringLabel;
private Text mNewIdTextField;
private Label mFileLabel;
/**
* Create the UI for the refactoring wizard.
* <p/>
* Note that at that point the initial conditions have been checked in
* {@link ExtractStringRefactoring}.
*/
public void createControl(Composite parent) {
final ExtractStringRefactoring ref = getOurRefactoring();
Composite content = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
content.setLayout(layout);
// line 1: String found in selection
Label label = new Label(content, SWT.NONE);
label.setText("String:");
String selectedString = ref.getTokenString();
mStringLabel = new Label(content, SWT.NONE);
mStringLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mStringLabel.setText(selectedString != null ? selectedString : "");
// TODO provide an option to replace all occurences of this string instead of
// just the one.
// line 2 : Textfield for new ID
label = new Label(content, SWT.NONE);
label.setText("Replace by R.string.");
mNewIdTextField = new Text(content, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
mNewIdTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mNewIdTextField.setText(guessId(selectedString));
ref.setReplacementStringId(mNewIdTextField.getText().trim());
mNewIdTextField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
if (validatePage(ref)) {
ref.setReplacementStringId(mNewIdTextField.getText().trim());
}
}
});
// line 3: selection of the output file
// TODO add a file field/chooser combo to let the user select the file to edit.
label = new Label(content, SWT.NONE);
label.setText("Resource file:");
mFileLabel = new Label(content, SWT.NONE);
mFileLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mFileLabel.setText("/res/values/strings.xml");
ref.setTargetFile(mFileLabel.getText());
// line 4: selection of the res config
// TODO add the Configuration Selector to decide with strings.xml to change
label = new Label(content, SWT.NONE);
label.setText("Configuration:");
label = new Label(content, SWT.NONE);
label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
label.setText("default");
validatePage(ref);
setControl(content);
}
private String guessId(String text) {
// 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;
}
private ExtractStringRefactoring getOurRefactoring() {
return (ExtractStringRefactoring) getRefactoring();
}
private boolean validatePage(ExtractStringRefactoring ref) {
String text = mNewIdTextField.getText().trim();
boolean success = true;
// Analyze fatal errors.
if (text == null || text.length() < 1) {
setErrorMessage("Please provide a resource ID to replace with.");
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;
}
}
}
// Analyze info & warnings.
if (success) {
if (ref.isResIdDuplicate(mFileLabel.getText(), text)) {
setErrorMessage(null);
setMessage(
String.format("Warning: There's already a string item called '%1$s' in %2$s.",
text, mFileLabel.getText()));
} else {
setMessage(null);
setErrorMessage(null);
}
}
setPageComplete(success);
return success;
}
}

View File

@@ -0,0 +1,890 @@
/*
* 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.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
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.resources.ResourceAttributes;
import org.eclipse.core.runtime.CoreException;
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.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 org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
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.HashSet;
import java.util.List;
import java.util.Map;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
/**
* This refactoring extracts a string from a file and replaces it by an Android resource ID
* such as R.string.foo.
* <p/>
* There are a number of scenarios, which are not all supported yet. The workflow works as
* such:
* <ul>
* <li> User selects a string in a Java (TODO: or XML file) and invokes
* the {@link ExtractStringAction}.
* <li> 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.
* <li> 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. <string>foo</string>) for values, etc.
* <li> 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.
* <li> TODO: Find the string in an XML file based on selection.
* <li> On success, the wizard is shown, which let the user input the new ID to use.
* <li> 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 #isResIdDuplicate(String, String)} to check whether the new ID is already defined
* in the target XML file.
* <li> 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.
* <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
* </ul>
*
* The list of changes are:
* <ul>
* <li> If the target XML does not exist, create it with the new string ID.
* <li> If the target XML exists, find the <resources> node and add the new string ID right after.
* If the node is <resources/>, it needs to be opened.
* <li> 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.
* <li> TODO: If the source is an XML file, determine if we need to change an attribute or a
* a text element.
* <li> TODO: Have a pref in the wizard: [x] Change other XML Files
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
* </ul>
*/
class ExtractStringRefactoring extends Refactoring {
/** The compilation unit, a.k.a. the Java file model. */
private final ICompilationUnit mUnit;
private final int mSelectionStart;
private final int mSelectionEnd;
/** The actual string selected, after UTF characters have been escaped, good for display. */
private String mTokenString;
/** Start position of the string token in the source buffer. */
private int mTokenStart;
/** End position of the string token in the source buffer. */
private int mTokenEnd;
private String mXmlStringId;
private String mTargetXmlFileWsPath;
private HashMap<String,HashSet<String>> mResIdCache;
private XPath mXPath;
private ArrayList<Change> mChanges;
public ExtractStringRefactoring(Map<String, String> arguments)
throws NullPointerException {
mUnit = (ICompilationUnit) JavaCore.create(arguments.get("CU")); //$NON-NLS-1$
mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
mTokenStart = Integer.parseInt(arguments.get("tok-start")); //$NON-NLS-1$
mTokenEnd = Integer.parseInt(arguments.get("tok-end")); //$NON-NLS-1$
mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
}
private Map<String, String> createArgumentMap() {
HashMap<String, String> args = new HashMap<String, String>();
args.put("CU", mUnit.getHandleIdentifier()); //$NON-NLS-1$
args.put("sel-start", Integer.toString(mSelectionStart)); //$NON-NLS-1$
args.put("sel-end", Integer.toString(mSelectionEnd)); //$NON-NLS-1$
args.put("tok-start", Integer.toString(mTokenStart)); //$NON-NLS-1$
args.put("tok-end", Integer.toString(mTokenEnd)); //$NON-NLS-1$
args.put("tok-esc", mTokenString); //$NON-NLS-1$
return args;
}
public ExtractStringRefactoring(ICompilationUnit unit, ITextSelection selection) {
mUnit = unit;
mSelectionStart = selection.getOffset();
mSelectionEnd = mSelectionStart + selection.getLength();
}
/**
* @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
*/
@Override
public String getName() {
return "Extract Android String";
}
/**
* Gets the actual string selected, after UTF characters have been escaped,
* good for display.
*/
public String getTokenString() {
return mTokenString;
}
/**
* 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.
* <p/>
* 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.
* <p/>
* 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 {
mTokenString = null;
mTokenStart = -1;
mTokenEnd = -1;
RefactoringStatus status = new RefactoringStatus();
try {
monitor.beginTask("Checking preconditions...", 3);
if (!extraChecks(monitor, status)) {
return status;
}
try {
IBuffer buffer = mUnit.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());
mTokenStart = scanner.getCurrentTokenStartPosition();
mTokenEnd = scanner.getCurrentTokenEndPosition();
}
break;
} else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
// scanner is past the selection, abort.
break;
}
}
} catch (JavaModelException e1) {
// Error in mUnit.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);
} finally {
monitor.done();
}
return status;
}
/**
* Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
* Might not be useful.
*
* @return False if caller should abort, true if caller should continue.
*/
private boolean extraChecks(IProgressMonitor monitor, RefactoringStatus status) {
//
IResource res = mUnit.getPrimary().getResource();
if (res == null || res.getType() != IResource.FILE) {
status.addFatalError("Cannot access resource; only regular files can be used.");
return false;
}
monitor.worked(1);
// check whether the source file is in sync
if (!res.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 = res.getResourceAttributes();
if (mUnit.isReadOnly() || 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.
* <p/>
* 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<Change>();
// Prepare the change for the XML file.
if (!isResIdDuplicate(mTargetXmlFileWsPath, mXmlStringId)) {
// We actually change it only if the ID doesn't exist yet
TextFileChange xmlChange = new TextFileChange(getName(), (IFile) targetXml);
xmlChange.setTextType("xml"); //$NON-NLS-1$
TextEdit edit = createXmlEdit((IFile) targetXml, mXmlStringId, mTokenString);
if (edit == null) {
status.addFatalError(String.format("Failed to modify file %1$s",
mTargetXmlFileWsPath));
}
xmlChange.setEdit(edit);
mChanges.add(xmlChange);
}
monitor.worked(1);
if (status.hasError()) {
return status;
}
// Prepare the change to the Java compilation unit
List<Change> 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 TextEdit} that adds the given
* ID to the given XML File.
* <p/>
* This does not actually modify the file.
*
* @param xmlFile The file resource to modify.
* @param replacementStringId The new ID to insert.
* @param oldString 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 TextEdit createXmlEdit(IFile xmlFile, String replacementStringId, String oldString) {
if (!xmlFile.exists()) {
// The XML file does not exist. Simply create it.
StringBuilder content = new StringBuilder();
content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
content.append("<resources>\n"); //$NON-NLS-1$
content.append(" <string name=\""). //$NON-NLS-1$
append(replacementStringId).
append("\">"). //$NON-NLS-1$
append(oldString).
append("</string>\n"); //$NON-NLS-1$
content.append("<resources>\n"); //$NON-NLS-1$
return new InsertEdit(0, content.toString());
}
// The file exist. Attempt to parse it as a valid XML document.
try {
int[] indices = new int[2];
if (findXmlOpeningTagPos(xmlFile.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(" <string name=\""). //$NON-NLS-1$
append(replacementStringId).
append("\">"). //$NON-NLS-1$
append(oldString).
append("</string>"); //$NON-NLS-1$
if (len == 2) {
content.append("\n</resources>"); //$NON-NLS-1$
}
return new ReplaceEdit(offset, len, content.toString());
}
} catch (CoreException e) {
// Failed to read file. Ignore. Will return null below.
}
return null;
}
/**
* Parse an XML input stream, looking for an opening tag.
* <p/>
* If found, returns the character offet in the buffer of the closing bracket of that
* tag, e.g. the position of > in "<resources>". The first character is at offset 0.
* <p/>
* 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.
* <p/>
* We need to deal with the case where the element is written as <resources/>, 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 <code>indices</code> 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 <?
if (!inComment && !inTag) {
if (c == '>' || 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;
}
private List<Change> 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;
IProject proj = unit.getJavaProject().getProject();
IResource manifestFile = proj.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<Change> changes = new ArrayList<Change>();
// 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 ir = ImportRewrite.create((CompilationUnit) node, true);
String Rqualifier = packageName + ".R"; //$NON-NLS-1$
Rqualifier = ir.addImport(Rqualifier);
// Rewrite the AST itself via an ASTVisitor
AST ast = node.getAST();
ASTRewrite ar = ASTRewrite.create(ast);
ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(ast, ar,
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 = ir.rewriteImports(subMonitor.newChild(1));
if (subEdit.hasChildren()) {
edit.addChild(subEdit);
}
// Create the edit to change the Java source, only if anything changed
subEdit = ar.rewriteAST();
if (subEdit.hasChildren()) {
edit.addChild(subEdit);
}
// Only create a change set if any edit was collected
if (edit.hasChildren()) {
change.setEdit(edit);
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;
}
} 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;
public ReplaceStringsVisitor(AST ast,
ASTRewrite astRewrite,
String oldString,
String rQualifier,
String xmlId) {
mAst = ast;
mRewriter = astRewrite;
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(getName());
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(
mUnit.getJavaProject().getElementName(), //project
comment, //description
comment, //comment
createArgumentMap());
return new RefactoringChangeDescriptor(desc);
}
};
monitor.worked(1);
return change;
} finally {
monitor.done();
}
}
/**
* 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 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(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<String, HashSet<String>>();
}
HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
if (cache == null) {
cache = getResIdsForFile(xmlFileWsPath);
mResIdCache.put(xmlFileWsPath, cache);
}
return cache.contains(stringId);
}
/**
* Extract all the defined string IDs from a given file using XPath.
*
* @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<String> getResIdsForFile(String xmlFileWsPath) {
HashSet<String> ids = new HashSet<String>();
if (mXPath == null) {
mXPath = AndroidXPathFactory.newXPath();
}
// Access the project that contains the resource that contains the compilation unit
IResource resource = getTargetXmlResource(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:
// <resources>
// <string name="ID">something</string>
// </resources>
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;
}
/**
* 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) {
IProject proj = mUnit.getPrimary().getResource().getProject();
Path path = new Path(xmlFileWsPath);
IResource resource = proj.findMember(path);
return resource;
}
/**
* Sets the replacement string ID. Used by the wizard to set the user input.
*/
public void setReplacementStringId(String replacementStringId) {
mXmlStringId = replacementStringId;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.ui.refactoring.RefactoringWizard;
/**
* A wizard for ExtractString based on a simple dialog with one page.
*
* @see ExtractStringInputPage
* @see ExtractStringRefactoring
*/
class ExtractStringWizard extends RefactoringWizard {
/**
* Create a wizard for ExtractString based on a simple dialog with one page.
*/
public ExtractStringWizard(ExtractStringRefactoring ref, String title) {
super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
setDefaultPageTitle(title);
}
@Override
protected void addUserInputPages() {
addPage(new ExtractStringInputPage());
}
}

View File

@@ -180,6 +180,8 @@ public class AndroidConstants {
public final static String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$
public final static String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$
public final static String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$
public final static String CLASS_INSTRUMENTATION_RUNNER =
"android.test.InstrumentationTestRunner"; //$NON-NLS-1$
public final static String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$
public final static String CLASS_R = "android.R"; //$NON-NLS-1$
public final static String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$
@@ -215,4 +217,5 @@ public class AndroidConstants {
/** The base URL where to find the Android class & manifest documentation */
public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$
public static final String LIBRARY_TEST_RUNNER = "android.test.runner"; // $NON-NLS-1$
}

View File

@@ -16,6 +16,7 @@
package com.android.ide.eclipse.common.project;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener;
import com.android.sdklib.SdkConstants;
@@ -255,12 +256,8 @@ public class AndroidManifestParser {
} catch (NumberFormatException e) {
handleError(e, -1 /* lineNumber */);
}
} else if (NODE_INSTRUMENTATION.equals(localName)) {
value = getAttributeValue(attributes, ATTRIBUTE_NAME,
true /* hasNamespace */);
if (value != null) {
mInstrumentations.add(value);
}
} else if (NODE_INSTRUMENTATION.equals(localName)) {
processInstrumentationNode(attributes);
}
break;
case LEVEL_ACTIVITY:
@@ -449,6 +446,25 @@ public class AndroidManifestParser {
addProcessName(processName);
}
}
/**
* Processes the instrumentation nodes.
* @param attributes the attributes for the activity node.
* node is representing
*/
private void processInstrumentationNode(Attributes attributes) {
// lets get the class name, and check it if required.
String instrumentationName = getAttributeValue(attributes, ATTRIBUTE_NAME,
true /* hasNamespace */);
if (instrumentationName != null) {
String instrClassName = combinePackageAndClassName(mPackage, instrumentationName);
mInstrumentations.add(instrClassName);
if (mMarkErrors) {
checkClass(instrClassName, AndroidConstants.CLASS_INSTRUMENTATION,
true /* testVisibility */);
}
}
}
/**
* Checks that a class is valid and can be used in the Android Manifest.
@@ -484,8 +500,7 @@ public class AndroidManifestParser {
} catch (CoreException e) {
}
}
}
}
}
/**
@@ -565,7 +580,6 @@ public class AndroidManifestParser {
ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
errorListener, gatherData, javaProject, markErrors);
parser.parse(new InputSource(manifestFile.getContents()), manifestHandler);
// get the result from the handler
@@ -576,14 +590,19 @@ public class AndroidManifestParser {
manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s", manifestFile.getFullPath());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s", manifestFile.getFullPath());
} catch (IOException e) {
} finally {
}
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s", manifestFile.getFullPath());
}
return null;
}
/**
* Parses the Android Manifest, and returns an object containing the result of the parsing.
* <p/>
@@ -619,11 +638,15 @@ public class AndroidManifestParser {
manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s", manifestFile.getAbsolutePath());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s", manifestFile.getAbsolutePath());
} catch (IOException e) {
} finally {
}
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s", manifestFile.getAbsolutePath());
}
return null;
}
@@ -646,10 +669,12 @@ public class AndroidManifestParser {
boolean gatherData,
boolean markErrors)
throws CoreException {
IFile manifestFile = getManifest(javaProject.getProject());
try {
SAXParser parser = sParserFactory.newSAXParser();
IFile manifestFile = getManifest(javaProject.getProject());
if (manifestFile != null) {
ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
errorListener, gatherData, javaProject, markErrors);
@@ -664,10 +689,15 @@ public class AndroidManifestParser {
manifestHandler.getInstrumentations(), manifestHandler.getUsesLibraries());
}
} catch (ParserConfigurationException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Bad parser configuration for %s", manifestFile.getFullPath());
} catch (SAXException e) {
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"Parser exception for %s", manifestFile.getFullPath());
} catch (IOException e) {
} finally {
}
AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
"I/O error for %s", manifestFile.getFullPath());
}
return null;
}

View File

@@ -61,8 +61,10 @@ import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.LanguageReg
import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.MobileCodeVerifier;
import com.android.layoutlib.api.ILayoutLog;
import com.android.layoutlib.api.ILayoutResult;
import com.android.layoutlib.api.IProjectCallback;
import com.android.layoutlib.api.IResourceValue;
import com.android.layoutlib.api.IStyleResourceValue;
import com.android.layoutlib.api.IXmlPullParser;
import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
import com.android.sdklib.IAndroidTarget;
@@ -222,7 +224,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// updateUiFromFramework will reset language/region combo, so we must call
// setConfiguration after, or the settext on language/region will be lost.
if (mEditedConfig != null) {
setConfiguration(mEditedConfig);
setConfiguration(mEditedConfig, false /*force*/);
}
// make sure we remove the custom view loader, since its parent class loader is the
@@ -867,7 +869,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
@Override
void editNewFile(FolderConfiguration configuration) {
// update the configuration UI
setConfiguration(configuration);
setConfiguration(configuration, true /*force*/);
// enable the create button if the current and edited config are not equals
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
@@ -975,18 +977,14 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
int themeIndex = mThemeCombo.getSelectionIndex();
if (themeIndex != -1) {
String theme = mThemeCombo.getItem(themeIndex);
// change the string if it's a custom theme to make sure we can
// differentiate them
if (themeIndex >= mPlatformThemeCount) {
theme = "*" + theme; //$NON-NLS-1$
}
// Render a single object as described by the ViewElementDescriptor.
WidgetPullParser parser = new WidgetPullParser(descriptor);
ILayoutResult result = bridge.bridge.computeLayout(parser,
ILayoutResult result = computeLayout(bridge, parser,
null /* projectKey */,
300 /* width */, 300 /* height */, theme,
300 /* width */, 300 /* height */, 160 /*density*/,
160.f /*xdpi*/, 160.f /*ydpi*/, theme,
themeIndex >= mPlatformThemeCount /*isProjectTheme*/,
configuredProjectResources, frameworkResources, projectCallback,
null /* logger */);
@@ -1073,11 +1071,14 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
/**
* Update the UI controls state with a given {@link FolderConfiguration}.
* <p/>If a qualifier is not present in the {@link FolderConfiguration} object, the UI control
* is not modified. However if the value in the control is not the default value, a warning
* icon is showed.
* <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect
* <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>,
* the UI control is not modified. However if the value in the control is not the default value,
* a warning icon is shown.
* @param config The {@link FolderConfiguration} to set.
* @param force Whether the UI should be changed to exactly match the received configuration.
*/
void setConfiguration(FolderConfiguration config) {
void setConfiguration(FolderConfiguration config, boolean force) {
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting new values in the widgets.
mEditedConfig = config;
@@ -1088,6 +1089,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (countryQualifier != null) {
mCountry.setText(String.format("%1$d", countryQualifier.getCode()));
mCurrentConfig.setCountryCodeQualifier(countryQualifier);
} else if (force) {
mCountry.setText(""); //$NON-NLS-1$
mCurrentConfig.setCountryCodeQualifier(null);
} else if (mCountry.getText().length() > 0) {
mCountryIcon.setImage(mWarningImage);
}
@@ -1097,6 +1101,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (networkQualifier != null) {
mNetwork.setText(String.format("%1$d", networkQualifier.getCode()));
mCurrentConfig.setNetworkCodeQualifier(networkQualifier);
} else if (force) {
mNetwork.setText(""); //$NON-NLS-1$
mCurrentConfig.setNetworkCodeQualifier(null);
} else if (mNetwork.getText().length() > 0) {
mNetworkIcon.setImage(mWarningImage);
}
@@ -1106,6 +1113,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (languageQualifier != null) {
mLanguage.setText(languageQualifier.getValue());
mCurrentConfig.setLanguageQualifier(languageQualifier);
} else if (force) {
mLanguage.setText(""); //$NON-NLS-1$
mCurrentConfig.setLanguageQualifier(null);
} else if (mLanguage.getText().length() > 0) {
mLanguageIcon.setImage(mWarningImage);
}
@@ -1115,6 +1125,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (regionQualifier != null) {
mRegion.setText(regionQualifier.getValue());
mCurrentConfig.setRegionQualifier(regionQualifier);
} else if (force) {
mRegion.setText(""); //$NON-NLS-1$
mCurrentConfig.setRegionQualifier(null);
} else if (mRegion.getText().length() > 0) {
mRegionIcon.setImage(mWarningImage);
}
@@ -1125,6 +1138,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mOrientation.select(
ScreenOrientation.getIndex(orientationQualifier.getValue()) + 1);
mCurrentConfig.setScreenOrientationQualifier(orientationQualifier);
} else if (force) {
mOrientation.select(0);
mCurrentConfig.setScreenOrientationQualifier(null);
} else if (mOrientation.getSelectionIndex() != 0) {
mOrientationIcon.setImage(mWarningImage);
}
@@ -1134,6 +1150,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (densityQualifier != null) {
mDensity.setText(String.format("%1$d", densityQualifier.getValue()));
mCurrentConfig.setPixelDensityQualifier(densityQualifier);
} else if (force) {
mDensity.setText(""); //$NON-NLS-1$
mCurrentConfig.setPixelDensityQualifier(null);
} else if (mDensity.getText().length() > 0) {
mDensityIcon.setImage(mWarningImage);
}
@@ -1143,6 +1162,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (touchQualifier != null) {
mTouch.select(TouchScreenType.getIndex(touchQualifier.getValue()) + 1);
mCurrentConfig.setTouchTypeQualifier(touchQualifier);
} else if (force) {
mTouch.select(0);
mCurrentConfig.setTouchTypeQualifier(null);
} else if (mTouch.getSelectionIndex() != 0) {
mTouchIcon.setImage(mWarningImage);
}
@@ -1152,6 +1174,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (keyboardQualifier != null) {
mKeyboard.select(KeyboardState.getIndex(keyboardQualifier.getValue()) + 1);
mCurrentConfig.setKeyboardStateQualifier(keyboardQualifier);
} else if (force) {
mKeyboard.select(0);
mCurrentConfig.setKeyboardStateQualifier(null);
} else if (mKeyboard.getSelectionIndex() != 0) {
mKeyboardIcon.setImage(mWarningImage);
}
@@ -1161,6 +1186,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (inputQualifier != null) {
mTextInput.select(TextInputMethod.getIndex(inputQualifier.getValue()) + 1);
mCurrentConfig.setTextInputMethodQualifier(inputQualifier);
} else if (force) {
mTextInput.select(0);
mCurrentConfig.setTextInputMethodQualifier(null);
} else if (mTextInput.getSelectionIndex() != 0) {
mTextInputIcon.setImage(mWarningImage);
}
@@ -1171,6 +1199,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mNavigation.select(
NavigationMethod.getIndex(navigationQualifiter.getValue()) + 1);
mCurrentConfig.setNavigationMethodQualifier(navigationQualifiter);
} else if (force) {
mNavigation.select(0);
mCurrentConfig.setNavigationMethodQualifier(null);
} else if (mNavigation.getSelectionIndex() != 0) {
mNavigationIcon.setImage(mWarningImage);
}
@@ -1181,6 +1212,10 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mSize1.setText(String.format("%1$d", sizeQualifier.getValue1()));
mSize2.setText(String.format("%1$d", sizeQualifier.getValue2()));
mCurrentConfig.setScreenDimensionQualifier(sizeQualifier);
} else if (force) {
mSize1.setText(""); //$NON-NLS-1$
mSize2.setText(""); //$NON-NLS-1$
mCurrentConfig.setScreenDimensionQualifier(null);
} else if (mSize1.getText().length() > 0 && mSize2.getText().length() > 0) {
mSizeIcon.setImage(mWarningImage);
}
@@ -1607,7 +1642,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// at this point, we have not opened a new file.
// update the configuration icons with the new edited config.
setConfiguration(mEditedConfig);
setConfiguration(mEditedConfig, false /*force*/);
// enable the create button if the current and edited config are not equals
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
@@ -1794,45 +1829,16 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// Compute the layout
UiElementPullParser parser = new UiElementPullParser(getModel());
Rectangle rect = getBounds();
ILayoutResult result = null;
if (bridge.apiLevel >= 3) {
// call the new api with proper theme differentiator and
// density/dpi support.
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
// FIXME pass the density/dpi from somewhere (resource config or skin).
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, 160, 160.f, 160.f,
theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else if (bridge.apiLevel == 2) {
// api with boolean for separation of project/framework theme
boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
// FIXME pass the density/dpi from somewhere (resource config or skin).
ILayoutResult result = computeLayout(bridge, parser,
iProject /* projectKey */,
rect.width, rect.height, 160, 160.f, 160.f,
theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, theme, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else {
// oldest api with no density/dpi, and project theme boolean mixed
// into the theme name.
// change the string if it's a custom theme to make sure we can
// differentiate them
if (themeIndex >= mPlatformThemeCount) {
theme = "*" + theme; //$NON-NLS-1$
}
result = bridge.bridge.computeLayout(parser,
iProject /* projectKey */,
rect.width, rect.height, theme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
}
// update the UiElementNode with the layout info.
if (result.getSuccess() == ILayoutResult.SUCCESS) {
model.setEditData(result.getImage());
@@ -2383,4 +2389,48 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
return null;
}
/**
* Computes a layout by calling the correct computeLayout method of ILayoutBridge based on
* the implementation API level.
*/
@SuppressWarnings("deprecation")
private ILayoutResult computeLayout(LayoutBridge bridge,
IXmlPullParser layoutDescription, Object projectKey,
int screenWidth, int screenHeight, int density, float xdpi, float ydpi,
String themeName, boolean isProjectTheme,
Map<String, Map<String, IResourceValue>> projectResources,
Map<String, Map<String, IResourceValue>> frameworkResources,
IProjectCallback projectCallback, ILayoutLog logger) {
if (bridge.apiLevel >= 3) {
// newer api with boolean for separation of project/framework theme,
// and density support.
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
themeName, isProjectTheme,
projectResources, frameworkResources, projectCallback,
logger);
} else if (bridge.apiLevel == 2) {
// api with boolean for separation of project/framework theme
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
} else {
// oldest api with no density/dpi, and project theme boolean mixed
// into the theme name.
// change the string if it's a custom theme to make sure we can
// differentiate them
if (isProjectTheme) {
themeName = "*" + themeName; //$NON-NLS-1$
}
return bridge.bridge.computeLayout(layoutDescription,
projectKey, screenWidth, screenHeight, themeName,
mConfiguredProjectRes, frameworkResources, mProjectCallback,
mLogger);
}
}
}