From 41373ab8b25e9f6cbc88c367de753d7c5bfa1752 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Thu, 19 Mar 2009 23:08:36 -0700 Subject: [PATCH] auto import from //branches/cupcake_rel/...@141571 --- .../META-INF/MANIFEST.MF | 4 +- .../plugins/com.android.ide.eclipse.adt/plugin.xml | 52 +- .../src/com/android/ide/eclipse/adt/AdtPlugin.java | 2 +- .../android/ide/eclipse/adt/build/ApkBuilder.java | 2 +- .../ide/eclipse/adt/build/PreCompilerBuilder.java | 2 +- .../eclipse/adt/build/ResourceManagerBuilder.java | 2 +- .../adt/launch/AndroidLaunchConfiguration.java | 31 +- .../adt/launch/AndroidLaunchController.java | 105 ++- .../ide/eclipse/adt/launch/EmulatorConfigTab.java | 14 +- .../eclipse/adt/launch/LaunchConfigDelegate.java | 42 +- .../eclipse/adt/launch/MainLaunchConfigTab.java | 2 +- .../adt/launch/junit/AndroidJUnitLaunchAction.java | 6 +- .../junit/AndroidJUnitLaunchConfigDelegate.java | 60 +- .../junit/AndroidJUnitLaunchConfigurationTab.java | 8 + .../launch/junit/AndroidJUnitLaunchShortcut.java | 30 +- ...ADTTestRunner.java => RemoteAdtTestRunner.java} | 4 +- .../ide/eclipse/adt/project/ProjectHelper.java | 71 ++ .../extractstring/ExtractStringAction.java | 161 ++++ .../extractstring/ExtractStringContribution.java | 53 ++ .../extractstring/ExtractStringDescriptor.java | 71 ++ .../extractstring/ExtractStringInputPage.java | 177 ++++ .../extractstring/ExtractStringRefactoring.java | 890 +++++++++++++++++++++ .../extractstring/ExtractStringWizard.java | 42 + .../ide/eclipse/common/AndroidConstants.java | 3 + .../common/project/AndroidManifestParser.java | 68 +- .../editors/layout/GraphicalLayoutEditor.java | 156 ++-- .../com/android/ide/eclipse/tests/AdtTestData.java | 29 +- .../common/project/AndroidManifestParserTest.java | 75 +- .../data/AndroidManifest-instrumentation.xml | 18 + .../unittests/data/AndroidManifest-testapp.xml | 17 + .../src/com/android/sdklib/avd/AvdManager.java | 20 +- 31 files changed, 2018 insertions(+), 199 deletions(-) rename eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/{RemoteADTTestRunner.java => RemoteAdtTestRunner.java} (98%) create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml create mode 100644 eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index 55c18bb26..c0dfcefd5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -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", diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index c18c72f05..2bf633d32 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -470,7 +470,7 @@ point="org.eclipse.ui.actionSets"> - - - - + + + + @@ -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"> @@ -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"> @@ -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"> @@ -565,4 +574,25 @@ + + + + + + + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index e0708f32b..42db64a61 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -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 } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java index f8a969e94..bc5b01c5f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java @@ -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 { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java index 8aa1abad8..6f9c2f16a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java @@ -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 { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java index 035aa5b73..b1f9ec1d4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java @@ -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 { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java index 448cda6b5..3e610db08 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java @@ -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. *
    @@ -41,7 +63,7 @@ public class AndroidLaunchConfiguration { *
  • false: manual mode
  • *
*/ - 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 } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java index 499cca704..3aa0b91f6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java @@ -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; } @@ -807,6 +833,81 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } /** + * 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 getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) { + List dependencies = new ArrayList(); + + // 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 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 * @param remotePath The remote path of the package. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java index b898f63c5..3789153e4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java @@ -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, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java index 80f62eaa8..d057ac709 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java @@ -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; @@ -388,39 +387,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. */ private String getActivityName(ILaunchConfiguration configuration) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java index 30b072389..91bd21cb7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java @@ -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$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java index 4dfe37d1d..b88026329 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java @@ -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); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java index 05cc6ae73..a624b0001 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java @@ -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 INSTRUMENTATION_OK 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 AndroidConstants.LIBRARY_TEST_RUNNER + * 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; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java index 5fbda983d..aa59a5157 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java @@ -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; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java index e03f2822b..30649e2e8 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java @@ -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); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java similarity index 98% rename from eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java rename to eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java index 6834c089a..0a6a3daee 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteADTTestRunner.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java @@ -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); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java index c650b9846..e091b1385 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java @@ -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 getAndroidProjectDependencies(IJavaProject javaProject) + throws JavaModelException { + String[] requiredProjectNames = javaProject.getRequiredProjectNames(); + + // Go from java project name to JavaProject name + IJavaModel javaModel = javaProject.getJavaModel(); + + // loop through all dependent projects and keep only those that are Android projects + List projectList = new ArrayList(requiredProjectNames.length); + for (String javaProjectName : requiredProjectNames) { + IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName); + + //Verify that the project has also the Android Nature + try { + if (!androidJavaProject.getProject().hasNature(AndroidConstants.NATURE)) { + continue; + } + } catch (CoreException e) { + continue; + } + + projectList.add(androidJavaProject); + } + + return projectList; + } + + /** + * Returns the android package file as an IFile object for the specified + * project. + * @param project The project + * @return The android package as an IFile object or null if not found. + */ + public static IFile getApplicationPackage(IProject project) { + // get the output folder + IFolder outputLocation = BaseProjectHelper.getOutputFolder(project); + + if (outputLocation == null) { + AdtPlugin.printErrorToConsole(project, + "Failed to get the output location of the project. Check build path properties" + ); + return null; + } + + + // get the package path + String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + IResource r = outputLocation.findMember(packageName); + + // check the package is present + if (r instanceof IFile && r.exists()) { + return (IFile)r; + } + + String msg = String.format("Could not find %1$s!", packageName); + AdtPlugin.printErrorToConsole(project, msg); + + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java new file mode 100644 index 000000000..528701573 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java @@ -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. + *

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

+ * Workflow: + *

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

          + * 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; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java new file mode 100644 index 000000000..465e1a36c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.refactorings.extractstring; + +import org.eclipse.ltk.core.refactoring.RefactoringContribution; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; + +import java.util.Map; + +/** + * @see ExtractStringDescriptor + */ +public class ExtractStringContribution extends RefactoringContribution { + + /* (non-Javadoc) + * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int) + */ + @SuppressWarnings("unchecked") + @Override + public RefactoringDescriptor createDescriptor( + String id, + String project, + String description, + String comment, + Map arguments, + int flags) + throws IllegalArgumentException { + return new ExtractStringDescriptor(project, description, comment, arguments); + } + + @SuppressWarnings("unchecked") + @Override + public Map retrieveArgumentMap(RefactoringDescriptor descriptor) { + if (descriptor instanceof ExtractStringDescriptor) { + return ((ExtractStringDescriptor) descriptor).getArguments(); + } + return super.retrieveArgumentMap(descriptor); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java new file mode 100644 index 000000000..6e999e942 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.refactorings.extractstring; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; + +import java.util.Map; + +/** + * A descriptor that allows an {@link ExtractStringRefactoring} to be created from + * a previous instance of itself. + */ +public class ExtractStringDescriptor extends RefactoringDescriptor { + + public static final String ID = + "com.android.ide.eclipse.adt.refactoring.extract.string"; //$NON-NLS-1$ + + private final Map mArguments; + + public ExtractStringDescriptor(String project, String description, String comment, + Map arguments) { + super(ID, project, description, comment, + RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags + ); + mArguments = arguments; + } + + public Map getArguments() { + return mArguments; + } + + /** + * Creates a new refactoring instance for this refactoring descriptor based on + * an argument map. The argument map is created by the refactoring itself in + * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)} + *

          + * This is apparently used to replay a refactoring. + * + * {@inheritDoc} + * + * @throws CoreException + */ + @Override + public Refactoring createRefactoring(RefactoringStatus status) throws CoreException { + try { + ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments); + return ref; + } catch (NullPointerException e) { + status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor"); + return null; + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java new file mode 100644 index 000000000..cb449f02e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java @@ -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. + *

          + * 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; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java new file mode 100644 index 000000000..715503c1f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java @@ -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. + *

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

            + *
          • User selects a string in a Java (TODO: or XML file) and invokes + * the {@link ExtractStringAction}. + *
          • The action finds the {@link ICompilationUnit} being edited as well as the current + * {@link ITextSelection}. The action creates a new instance of this refactoring as + * well as an {@link ExtractStringWizard} and runs the operation. + *
          • TODO: to support refactoring from an XML file, the action should give the {@link IFile} + * and then here we would have to determine whether it's a suitable Android XML file or a + * suitable Java file. + * TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout + * files or text elements (e.g. foo) for values, etc. + *
          • Step 1 of the refactoring is to check the preliminary conditions. Right now we check + * that the java source is not read-only and is in sync. We also try to find a string under + * the selection. If this fails, the refactoring is aborted. + *
          • TODO: Find the string in an XML file based on selection. + *
          • On success, the wizard is shown, which let the user input the new ID to use. + *
          • The wizard sets the user input values into this refactoring instance, e.g. the new string + * ID, the XML file to update, etc. The wizard does use the utility method + * {@link #isResIdDuplicate(String, String)} to check whether the new ID is already defined + * in the target XML file. + *
          • Once Preview or Finish is selected in the wizard, the + * {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input + * and compute the actual changes. + *
          • When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked. + *
          + * + * The list of changes are: + *
            + *
          • If the target XML does not exist, create it with the new string ID. + *
          • If the target XML exists, find the node and add the new string ID right after. + * If the node is , it needs to be opened. + *
          • Create an AST rewriter to edit the source Java file and replace all occurences by the + * new computed R.string.foo. Also need to rewrite imports to import R as needed. + * If there's already a conflicting R included, we need to insert the FQCN instead. + *
          • TODO: If the source is an XML file, determine if we need to change an attribute or a + * a text element. + *
          • TODO: Have a pref in the wizard: [x] Change other XML Files + *
          • TODO: Have a pref in the wizard: [x] Change other Java Files + *
          + */ +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> mResIdCache; + private XPath mXPath; + private ArrayList mChanges; + + public ExtractStringRefactoring(Map 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 createArgumentMap() { + HashMap args = new HashMap(); + 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. + *

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

          + * This is also used to extract the string to be modified, so that we can display it in + * the refactoring wizard. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + + 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. + *

          + * In this case, most of the sanity checks are done by the wizard so essentially this + * should only be called if the wizard positively validated the user input. + * + * Here we do check that the target resource XML file either does not exists or + * is not read-only. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + RefactoringStatus status = new RefactoringStatus(); + + try { + monitor.beginTask("Checking post-conditions...", 3); + + if (mXmlStringId == null || mXmlStringId.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing replacement string ID"); + } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing target xml file path"); + } + monitor.worked(1); + + // Either that resource must not exist or it must be a writeable file. + IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath); + if (targetXml != null) { + if (targetXml.getType() != IResource.FILE) { + status.addFatalError( + String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath)); + } else { + ResourceAttributes attr = targetXml.getResourceAttributes(); + if (attr != null && attr.isReadOnly()) { + status.addFatalError( + String.format("XML file '%1$s' is read-only.", + mTargetXmlFileWsPath)); + } + } + } + monitor.worked(1); + + if (status.hasError()) { + return status; + } + + mChanges = new ArrayList(); + + + // Prepare the change for the XML file. + + if (!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 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. + *

          + * 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("\n"); //$NON-NLS-1$ + content.append("\n"); //$NON-NLS-1$ + content.append(" "). //$NON-NLS-1$ + append(oldString). + append("\n"); //$NON-NLS-1$ + content.append("\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(" "). //$NON-NLS-1$ + append(oldString). + append(""); //$NON-NLS-1$ + if (len == 2) { + content.append("\n"); //$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. + *

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

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

          + * We need to deal with the case where the element is written as , in + * which case the caller will want to replace /> by ">...". To do that we return + * two values: the first offset of the closing tag (e.g. / or >) and the length, which + * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >. + * + * @param contents An existing buffer to parse. + * @param tag The tag to look for. + * @param indices The return values: [0] is the offset of the closing bracket and [1] is + * the length which can be only 1 for > and 2 for /> + * @return True if we found the tag, in which case indices can be used. + */ + private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) { + + BufferedReader br = new BufferedReader(new InputStreamReader(contents)); + StringBuilder sb = new StringBuilder(); // scratch area + + tag = "<" + tag; + int tagLen = tag.length(); + int maxLen = tagLen < 3 ? 3 : tagLen; + + try { + int offset = 0; + int i = 0; + char searching = '<'; // we want opening tags + boolean capture = false; + boolean inComment = false; + boolean inTag = false; + while ((i = br.read()) != -1) { + char c = (char) i; + if (c == searching) { + capture = true; + } + if (capture) { + sb.append(c); + int len = sb.length(); + if (inComment && c == '>') { + // is the comment being closed? + if (len >= 3 && sb.substring(len-3).equals("-->")) { //$NON-NLS-1$ + // yes, comment is closing, stop capturing + capture = false; + inComment = false; + sb.setLength(0); + } + } else if (inTag && c == '>') { + // we're capturing in our tag, waiting for the closing >, we just got it + // so we're totally done here. Simply detect whether it's /> or >. + indices[0] = offset; + indices[1] = 1; + if (sb.charAt(len - 2) == '/') { + indices[0]--; + indices[1]++; + } + return true; + + } else if (!inComment && !inTag) { + // not a comment and not our tag yet, so we're capturing because a + // tag is being opened but we don't know which one yet. + + // look for either the opening or a comment or + // the opening of our tag. + if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$ + inComment = true; + } else if (len == tagLen && sb.toString().equals(tag)) { + inTag = true; + } + + // if we're not interested in this tag yet, deal with when to stop + // capturing: the opening tag ends with either any kind of whitespace + // or with a > or maybe there's a PI that starts with ' || c == '?' || c == ' ' || c == '\n' || c == '\r') { + // stop capturing + capture = false; + sb.setLength(0); + } + } + } + + if (capture && len > maxLen) { + // in any case we don't need to capture more than the size of our tag + // or the comment opening tag + sb.deleteCharAt(0); + } + } + offset++; + } + } catch (IOException e) { + // Ignore. + } finally { + try { + br.close(); + } catch (IOException e) { + // oh come on... + } + } + + return false; + } + + private List computeJavaChanges(ICompilationUnit unit, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor subMonitor) { + + // Get the Android package name from the Android Manifest. We need it to create + // the FQCN of the R class. + String packageName = null; + String error = null; + 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 changes = new ArrayList(); + + // This is the unit that will be modified. + TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource()); + change.setTextType("java"); //$NON-NLS-1$ + + // Create an AST for this compilation unit + ASTParser parser = ASTParser.newParser(AST.JLS3); + parser.setProject(unit.getJavaProject()); + parser.setSource(unit); + parser.setResolveBindings(true); + ASTNode node = parser.createAST(subMonitor.newChild(1)); + + // The ASTNode must be a CompilationUnit, by design + if (!(node instanceof CompilationUnit)) { + status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$ + node.getClass())); + return null; + } + + // ImportRewrite will allow us to add the new type to the imports and will resolve + // what the Java source must reference, e.g. the FQCN or just the simple name. + ImportRewrite 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>(); + } + HashSet 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 getResIdsForFile(String xmlFileWsPath) { + HashSet ids = new HashSet(); + + if (mXPath == null) { + mXPath = AndroidXPathFactory.newXPath(); + } + + // Access the project that contains the resource that contains the compilation unit + IResource resource = 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: + // + // something + // + + String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$ + + Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET); + if (result instanceof NodeList) { + NodeList list = (NodeList) result; + for (int n = list.getLength() - 1; n >= 0; n--) { + String id = list.item(n).getNodeValue(); + ids.add(id); + } + } + + } catch (CoreException e1) { + // IFile.getContents failed. Ignore. + } catch (XPathExpressionException e) { + // mXPath.evaluate failed. Ignore. + } + } + + return ids; + } + + /** + * 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; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java new file mode 100644 index 000000000..2083a6e5d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java @@ -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()); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java index 5abfd811d..1da753c7b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java @@ -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$ } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java index 42c881b15..fa7e9b9f4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java @@ -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. *

          @@ -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; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java index 9c529e5da..12d49fe27 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java @@ -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}. - *

          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. + *

          If force is set to true the UI will be changed to exactly reflect + * config, otherwise, if a qualifier is not present in config, + * 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; - - // 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; - - 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. + 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); - // 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> projectResources, + Map> 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); + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java index 262ef65a1..d86d585a3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java @@ -15,7 +15,13 @@ */ package com.android.ide.eclipse.tests; +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Platform; + import java.io.File; +import java.io.IOException; import java.net.URL; import java.util.logging.Logger; @@ -45,12 +51,29 @@ public class AdtTestData { // accessed normally mOsRootDataPath = System.getProperty("test_data"); if (mOsRootDataPath == null) { - sLogger.info("Cannot find test_data directory, init to class loader"); + sLogger.info("Cannot find test_data environment variable, init to class loader"); URL url = this.getClass().getClassLoader().getResource("data"); //$NON-NLS-1$ - mOsRootDataPath = url.getFile(); + + if (Platform.isRunning()) { + sLogger.info("Running as an Eclipse Plug-in JUnit test, using FileLocator"); + try { + mOsRootDataPath = FileLocator.resolve(url).getFile(); + } catch (IOException e) { + sLogger.warning("IOException while using FileLocator, reverting to url"); + mOsRootDataPath = url.getFile(); + } + } else { + sLogger.info("Running as an plain JUnit test, using url as-is"); + mOsRootDataPath = url.getFile(); + } + } + + if (mOsRootDataPath.equals(AndroidConstants.WS_SEP + "data")) { + sLogger.warning("Resource data not found using class loader!, Defaulting to no path"); } + if (!mOsRootDataPath.endsWith(File.separator)) { - sLogger.info("Fixing test_data env variable does not end with path separator"); + sLogger.info("Fixing test_data env variable (does not end with path separator)"); mOsRootDataPath = mOsRootDataPath.concat(File.separator); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java index 516e448e6..7e8b0af10 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java @@ -16,17 +16,20 @@ package com.android.ide.eclipse.common.project; -import junit.framework.TestCase; +import com.android.ide.eclipse.tests.AdtTestData; -import com.android.ide.eclipse.mock.FileMock; +import junit.framework.TestCase; /** * Tests for {@link AndroidManifestParser} */ public class AndroidManifestParserTest extends TestCase { - private AndroidManifestParser mManifest; + private AndroidManifestParser mManifestTestApp; + private AndroidManifestParser mManifestInstrumentation; - private static final String PACKAGE_NAME = "com.android.testapp"; //$NON-NLS-1$ + private static final String INSTRUMENTATION_XML = "AndroidManifest-instrumentation.xml"; //$NON-NLS-1$ + private static final String TESTAPP_XML = "AndroidManifest-testapp.xml"; //$NON-NLS-1$ + private static final String PACKAGE_NAME = "com.android.testapp"; //$NON-NLS-1$ private static final String ACTIVITY_NAME = "com.android.testapp.MainActivity"; //$NON-NLS-1$ private static final String LIBRARY_NAME = "android.test.runner"; //$NON-NLS-1$ private static final String INSTRUMENTATION_NAME = "android.test.InstrumentationTestRunner"; //$NON-NLS-1$ @@ -35,60 +38,46 @@ public class AndroidManifestParserTest extends TestCase { protected void setUp() throws Exception { super.setUp(); - // create the test data - StringBuilder sb = new StringBuilder(); - sb.append("\n"); //$NON-NLS-1$ - sb.append("\n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \"\n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" \n"); //$NON-NLS-1$ - sb.append(" "); //$NON-NLS-1$ - sb.append(" \n"); - sb.append("\n"); //$NON-NLS-1$ - - FileMock mockFile = new FileMock("AndroidManifest.xml", sb.toString().getBytes()); + String testFilePath = AdtTestData.getInstance().getTestFilePath( + TESTAPP_XML); + mManifestTestApp = AndroidManifestParser.parseForData(testFilePath); + assertNotNull(mManifestTestApp); - mManifest = AndroidManifestParser.parseForData(mockFile); - assertNotNull(mManifest); + testFilePath = AdtTestData.getInstance().getTestFilePath( + INSTRUMENTATION_XML); + mManifestInstrumentation = AndroidManifestParser.parseForData(testFilePath); + assertNotNull(mManifestInstrumentation); } + public void testGetInstrumentationInformation() { + assertEquals(1, mManifestInstrumentation.getInstrumentations().length); + assertEquals(INSTRUMENTATION_NAME, mManifestTestApp.getInstrumentations()[0]); + } + public void testGetPackage() { - assertEquals("com.android.testapp", mManifest.getPackage()); + assertEquals(PACKAGE_NAME, mManifestTestApp.getPackage()); } public void testGetActivities() { - assertEquals(1, mManifest.getActivities().length); - assertEquals(ACTIVITY_NAME, mManifest.getActivities()[0]); + assertEquals(1, mManifestTestApp.getActivities().length); + assertEquals(ACTIVITY_NAME, mManifestTestApp.getActivities()[0]); } public void testGetLauncherActivity() { - assertEquals(ACTIVITY_NAME, mManifest.getLauncherActivity()); + assertEquals(ACTIVITY_NAME, mManifestTestApp.getLauncherActivity()); } public void testGetUsesLibraries() { - assertEquals(1, mManifest.getUsesLibraries().length); - assertEquals(LIBRARY_NAME, mManifest.getUsesLibraries()[0]); + assertEquals(1, mManifestTestApp.getUsesLibraries().length); + assertEquals(LIBRARY_NAME, mManifestTestApp.getUsesLibraries()[0]); } public void testGetInstrumentations() { - assertEquals(1, mManifest.getInstrumentations().length); - assertEquals(INSTRUMENTATION_NAME, mManifest.getInstrumentations()[0]); + assertEquals(1, mManifestTestApp.getInstrumentations().length); + assertEquals(INSTRUMENTATION_NAME, mManifestTestApp.getInstrumentations()[0]); + } + + public void testGetPackageName() { + assertEquals(PACKAGE_NAME, mManifestTestApp.getPackage()); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml new file mode 100644 index 000000000..b380f967e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml new file mode 100644 index 000000000..8ae70121c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml @@ -0,0 +1,17 @@ + + + + + + + " + + + + + " + + \ No newline at end of file diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java index 65cbbe356..93577e42b 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java @@ -177,7 +177,7 @@ public final class AvdManager { public AvdManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException { mSdk = sdk; mSdkLog = sdkLog; - buildAvdList(); + buildAvdList(mAvdList); } /** @@ -201,6 +201,20 @@ public final class AvdManager { return null; } + + /** + * Reloads the AVD list. + * @throws AndroidLocationException if there was an error finding the location of the + * AVD folder. + */ + public void reloadAvds() throws AndroidLocationException { + // build the list in a temp list first, in case the method throws an exception. + // It's better than deleting the whole list before reading the new one. + ArrayList list = new ArrayList(); + buildAvdList(list); + mAvdList.clear(); + mAvdList.addAll(list); + } /** * Creates a new AVD. It is expected that there is no existing AVD with this name already. @@ -620,7 +634,7 @@ public final class AvdManager { } } - private void buildAvdList() throws AndroidLocationException { + private void buildAvdList(ArrayList list) throws AndroidLocationException { // get the Android prefs location. String avdRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; @@ -664,7 +678,7 @@ public final class AvdManager { for (File avd : avds) { AvdInfo info = parseAvdInfo(avd); if (info != null) { - mAvdList.add(info); + list.add(info); if (avdListDebug) { mSdkLog.printf("[AVD LIST DEBUG] Added AVD '%s'\n", info.getPath()); } -- 2.11.0