summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/CustomLocale/NOTICE190
-rw-r--r--apps/Development/NOTICE190
-rw-r--r--apps/SdkSetup/NOTICE190
-rw-r--r--apps/SpareParts/NOTICE190
-rw-r--r--apps/SpareParts/res/values/strings.xml4
-rw-r--r--apps/SpareParts/res/xml/spare_parts.xml6
-rw-r--r--apps/SpareParts/src/com/android/spare_parts/SpareParts.java10
-rw-r--r--apps/launchperf/NOTICE190
-rw-r--r--cmds/monkey/src/com/android/commands/monkey/Monkey.java14
-rw-r--r--cmds/monkey/src/com/android/commands/monkey/MonkeyEvent.java1
-rw-r--r--cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java14
-rw-r--r--cmds/monkey/src/com/android/commands/monkey/MonkeyThrottleEvent.java52
-rw-r--r--emulator/qemud/Android.mk1
-rw-r--r--emulator/qemud/qemud.c1363
-rw-r--r--emulator/sensors/Android.mk29
-rw-r--r--emulator/sensors/sensors_qemu.c591
-rw-r--r--pdk/README5
-rw-r--r--samples/ApiDemos/AndroidManifest.xml16
-rw-r--r--samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java16
-rw-r--r--samples/ApiDemos/src/com/example/android/apis/graphics/TranslucentGLSurfaceViewActivity.java4
-rw-r--r--samples/ApiDemos/src/com/example/android/apis/graphics/TriangleActivity.java1
-rw-r--r--samples/ApiDemos/src/com/example/android/apis/graphics/TriangleRenderer.java10
-rw-r--r--samples/ApiDemos/src/com/example/android/apis/graphics/kube/KubeRenderer.java9
-rw-r--r--samples/Compass/src/com/example/android/compass/CompassActivity.java10
-rw-r--r--samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java2
-rw-r--r--testrunner/Android.mk19
-rw-r--r--testrunner/android_build.py45
-rw-r--r--testrunner/coverage_targets.xml4
-rwxr-xr-xtestrunner/runtest.py280
-rw-r--r--testrunner/tests.xml20
-rw-r--r--tools/anttasks/src/com/android/ant/AaptExecLoopTask.java11
-rwxr-xr-xtools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java82
-rw-r--r--tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java148
-rw-r--r--tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java51
-rw-r--r--tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java21
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF5
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.pngbin0 -> 393 bytes
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml101
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java21
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java12
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java27
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java21
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java2
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java44
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java129
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java11
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java14
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java42
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java24
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java276
-rwxr-xr-xtools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java134
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java977
-rwxr-xr-xtools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java56
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java42
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/InstrumentationRunnerValidator.java145
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java60
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java63
-rwxr-xr-xtools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java222
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java75
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java124
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java78
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java71
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java9
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java170
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java53
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java71
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java477
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java965
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java50
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java7
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java60
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java15
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java117
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java25
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java12
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java13
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java156
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java120
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java2
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java2
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java18
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java105
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java29
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java75
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java8
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java37
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java44
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml18
-rw-r--r--tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml17
-rw-r--r--tools/scripts/android_rules.xml1
-rwxr-xr-xtools/scripts/divide_and_compress.py240
-rw-r--r--tools/scripts/divide_and_compress_constants.py76
-rwxr-xr-xtools/scripts/divide_and_compress_test.py (renamed from tools/scripts/test_divide_and_compress.py)37
-rw-r--r--tools/sdkmanager/app/src/com/android/sdkmanager/Main.java51
-rw-r--r--tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java2
-rw-r--r--tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java20
-rw-r--r--tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java33
97 files changed, 8520 insertions, 1180 deletions
diff --git a/apps/CustomLocale/NOTICE b/apps/CustomLocale/NOTICE
new file mode 100644
index 000000000..c5b1efa7a
--- /dev/null
+++ b/apps/CustomLocale/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/apps/Development/NOTICE b/apps/Development/NOTICE
new file mode 100644
index 000000000..c5b1efa7a
--- /dev/null
+++ b/apps/Development/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/apps/SdkSetup/NOTICE b/apps/SdkSetup/NOTICE
new file mode 100644
index 000000000..c5b1efa7a
--- /dev/null
+++ b/apps/SdkSetup/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/apps/SpareParts/NOTICE b/apps/SpareParts/NOTICE
new file mode 100644
index 000000000..c5b1efa7a
--- /dev/null
+++ b/apps/SpareParts/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/apps/SpareParts/res/values/strings.xml b/apps/SpareParts/res/values/strings.xml
index 6aecca826..7bd13e54a 100644
--- a/apps/SpareParts/res/values/strings.xml
+++ b/apps/SpareParts/res/values/strings.xml
@@ -45,6 +45,10 @@
<string name="summary_on_fancy_ime_animations">Use fancier animations for input method windows</string>
<string name="summary_off_fancy_ime_animations">Use normal animations for input method windows</string>
+ <string name="title_fancy_rotation_animations">Fancy rotation animations</string>
+ <string name="summary_on_fancy_rotation_animations">Use fancier animations for screen rotation</string>
+ <string name="summary_off_fancy_rotation_animations">Use normal animations for screen rotation</string>
+
<string name="title_haptic_feedback">Haptic feedback</string>
<string name="summary_on_haptic_feedback">Use haptic feedback with user interaction</string>
<string name="summary_off_haptic_feedback">Use haptic feedback with user interaction</string>
diff --git a/apps/SpareParts/res/xml/spare_parts.xml b/apps/SpareParts/res/xml/spare_parts.xml
index 3e06397a6..3ef1b7961 100644
--- a/apps/SpareParts/res/xml/spare_parts.xml
+++ b/apps/SpareParts/res/xml/spare_parts.xml
@@ -72,6 +72,12 @@
android:summaryOn="@string/summary_on_fancy_ime_animations"
android:summaryOff="@string/summary_off_fancy_ime_animations"/>
+ <CheckBoxPreference
+ android:key="fancy_rotation_animations"
+ android:title="@string/title_fancy_rotation_animations"
+ android:summaryOn="@string/summary_on_fancy_rotation_animations"
+ android:summaryOff="@string/summary_off_fancy_rotation_animations"/>
+
<ListPreference
android:key="font_size"
android:title="@string/title_font_size"
diff --git a/apps/SpareParts/src/com/android/spare_parts/SpareParts.java b/apps/SpareParts/src/com/android/spare_parts/SpareParts.java
index 2507746a3..c00cc516a 100644
--- a/apps/SpareParts/src/com/android/spare_parts/SpareParts.java
+++ b/apps/SpareParts/src/com/android/spare_parts/SpareParts.java
@@ -54,6 +54,7 @@ public class SpareParts extends PreferenceActivity
private static final String WINDOW_ANIMATIONS_PREF = "window_animations";
private static final String TRANSITION_ANIMATIONS_PREF = "transition_animations";
private static final String FANCY_IME_ANIMATIONS_PREF = "fancy_ime_animations";
+ private static final String FANCY_ROTATION_ANIMATIONS_PREF = "fancy_rotation_animations";
private static final String HAPTIC_FEEDBACK_PREF = "haptic_feedback";
private static final String FONT_SIZE_PREF = "font_size";
private static final String END_BUTTON_PREF = "end_button";
@@ -64,6 +65,7 @@ public class SpareParts extends PreferenceActivity
private ListPreference mWindowAnimationsPref;
private ListPreference mTransitionAnimationsPref;
private CheckBoxPreference mFancyImeAnimationsPref;
+ private CheckBoxPreference mFancyRotationAnimationsPref;
private CheckBoxPreference mHapticFeedbackPref;
private ListPreference mFontSizePref;
private ListPreference mEndButtonPref;
@@ -118,6 +120,7 @@ public class SpareParts extends PreferenceActivity
mTransitionAnimationsPref = (ListPreference) prefSet.findPreference(TRANSITION_ANIMATIONS_PREF);
mTransitionAnimationsPref.setOnPreferenceChangeListener(this);
mFancyImeAnimationsPref = (CheckBoxPreference) prefSet.findPreference(FANCY_IME_ANIMATIONS_PREF);
+ mFancyRotationAnimationsPref = (CheckBoxPreference) prefSet.findPreference(FANCY_ROTATION_ANIMATIONS_PREF);
mHapticFeedbackPref = (CheckBoxPreference) prefSet.findPreference(HAPTIC_FEEDBACK_PREF);
mFontSizePref = (ListPreference) prefSet.findPreference(FONT_SIZE_PREF);
mFontSizePref.setOnPreferenceChangeListener(this);
@@ -143,6 +146,9 @@ public class SpareParts extends PreferenceActivity
mFancyImeAnimationsPref.setChecked(Settings.System.getInt(
getContentResolver(),
Settings.System.FANCY_IME_ANIMATIONS, 0) != 0);
+ mFancyRotationAnimationsPref.setChecked(Settings.System.getInt(
+ getContentResolver(),
+ "fancy_rotation_anim", 0) != 0);
mHapticFeedbackPref.setChecked(Settings.System.getInt(
getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0);
@@ -241,6 +247,10 @@ public class SpareParts extends PreferenceActivity
Settings.System.putInt(getContentResolver(),
Settings.System.FANCY_IME_ANIMATIONS,
mFancyImeAnimationsPref.isChecked() ? 1 : 0);
+ } else if (FANCY_ROTATION_ANIMATIONS_PREF.equals(key)) {
+ Settings.System.putInt(getContentResolver(),
+ "fancy_rotation_anim",
+ mFancyRotationAnimationsPref.isChecked() ? 1 : 0);
} else if (HAPTIC_FEEDBACK_PREF.equals(key)) {
Settings.System.putInt(getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED,
diff --git a/apps/launchperf/NOTICE b/apps/launchperf/NOTICE
new file mode 100644
index 000000000..c5b1efa7a
--- /dev/null
+++ b/apps/launchperf/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/cmds/monkey/src/com/android/commands/monkey/Monkey.java b/cmds/monkey/src/com/android/commands/monkey/Monkey.java
index f6ab19d73..00fb40cb9 100644
--- a/cmds/monkey/src/com/android/commands/monkey/Monkey.java
+++ b/cmds/monkey/src/com/android/commands/monkey/Monkey.java
@@ -369,7 +369,7 @@ public class Monkey {
if (mVerbose >= 2) { // check seeding performance
System.out.println("// Seeded: " + mSeed);
}
- mEventSource = new MonkeySourceRandom(mSeed, mMainApps);
+ mEventSource = new MonkeySourceRandom(mSeed, mMainApps, mThrottle);
mEventSource.setVerbose(mVerbose);
//set any of the factors that has been set
for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
@@ -709,13 +709,6 @@ public class Monkey {
}
}
- try {
- Thread.sleep(mThrottle);
- } catch (InterruptedException e1) {
- System.out.println("** Monkey interrupted in sleep.");
- return i;
- }
-
// In this debugging mode, we never send any events. This is primarily
// here so you can manually test the package or category limits, while manually
// exercising the system.
@@ -730,7 +723,10 @@ public class Monkey {
MonkeyEvent ev = mEventSource.getNextEvent();
if (ev != null) {
- i++;
+ // We don't want to count throttling as an event.
+ if (!(ev instanceof MonkeyThrottleEvent)) {
+ i++;
+ }
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
if (injectCode == MonkeyEvent.INJECT_FAIL) {
if (ev instanceof MonkeyKeyEvent) {
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyEvent.java
index ff99f5f06..7176073b9 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyEvent.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyEvent.java
@@ -29,6 +29,7 @@ public abstract class MonkeyEvent {
public static final int EVENT_TYPE_TRACKBALL = 2;
public static final int EVENT_TYPE_ACTIVITY = 3;
public static final int EVENT_TYPE_FLIP = 4; // Keyboard flip
+ public static final int EVENT_TYPE_THROTTLE = 5;
public static final int INJECT_SUCCESS = 1;
public static final int INJECT_FAIL = 0;
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java
index 3dbb575aa..902d8e84c 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java
@@ -171,6 +171,7 @@ public class MonkeySourceRandom implements MonkeyEventSource{
private LinkedList<MonkeyEvent> mQ = new LinkedList<MonkeyEvent>();
private Random mRandom;
private int mVerbose = 0;
+ private long mThrottle = 0;
private boolean mKeyboardOpen = false;
@@ -185,7 +186,7 @@ public class MonkeySourceRandom implements MonkeyEventSource{
return KEY_NAMES[keycode];
}
- public MonkeySourceRandom(long seed, ArrayList<ComponentName> MainApps) {
+ public MonkeySourceRandom(long seed, ArrayList<ComponentName> MainApps, long throttle) {
// default values for random distributions
// note, these are straight percentages, to match user input (cmd line args)
// but they will be converted to 0..1 values before the main loop runs.
@@ -202,6 +203,7 @@ public class MonkeySourceRandom implements MonkeyEventSource{
mRandom = new SecureRandom();
mRandom.setSeed((seed == 0) ? -1 : seed);
mMainApps = MainApps;
+ mThrottle = throttle;
}
/**
@@ -334,6 +336,7 @@ public class MonkeySourceRandom implements MonkeyEventSource{
downAt, MotionEvent.ACTION_UP, x, y, 0);
e.setIntermediateNote(false);
mQ.addLast(e);
+ addThrottle();
}
/**
@@ -384,6 +387,7 @@ public class MonkeySourceRandom implements MonkeyEventSource{
e.setIntermediateNote(false);
mQ.addLast(e);
}
+ addThrottle();
}
/**
@@ -416,11 +420,13 @@ public class MonkeySourceRandom implements MonkeyEventSource{
MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
mQ.addLast(e);
+ addThrottle();
return;
} else if (cls < mFactors[FACTOR_FLIP]) {
MonkeyFlipEvent e = new MonkeyFlipEvent(mKeyboardOpen);
mKeyboardOpen = !mKeyboardOpen;
mQ.addLast(e);
+ addThrottle();
return;
} else {
lastKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1);
@@ -431,6 +437,8 @@ public class MonkeySourceRandom implements MonkeyEventSource{
e = new MonkeyKeyEvent(KeyEvent.ACTION_UP, lastKey);
mQ.addLast(e);
+
+ addThrottle();
}
public boolean validate() {
@@ -464,4 +472,8 @@ public class MonkeySourceRandom implements MonkeyEventSource{
mQ.removeFirst();
return e;
}
+
+ private void addThrottle() {
+ mQ.addLast(new MonkeyThrottleEvent(MonkeyEvent.EVENT_TYPE_THROTTLE, mThrottle));
+ }
}
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyThrottleEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyThrottleEvent.java
new file mode 100644
index 000000000..3d7d48aa5
--- /dev/null
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyThrottleEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.commands.monkey;
+
+import android.app.IActivityManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.view.IWindowManager;
+import android.view.MotionEvent;
+
+
+/**
+ * monkey throttle event
+ */
+public class MonkeyThrottleEvent extends MonkeyEvent {
+ private long mThrottle;
+
+ public MonkeyThrottleEvent(int type, long throttle) {
+ super(type);
+ mThrottle = throttle;
+ }
+
+ @Override
+ public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
+
+ if (verbose > 1) {
+ System.out.println("Sleeping for " + mThrottle + " milliseconds");
+ }
+ try {
+ Thread.sleep(mThrottle);
+ } catch (InterruptedException e1) {
+ System.out.println("** Monkey interrupted in sleep.");
+ return MonkeyEvent.INJECT_FAIL;
+ }
+
+ return MonkeyEvent.INJECT_SUCCESS;
+ }
+}
diff --git a/emulator/qemud/Android.mk b/emulator/qemud/Android.mk
index 454f32dda..a186c73de 100644
--- a/emulator/qemud/Android.mk
+++ b/emulator/qemud/Android.mk
@@ -11,5 +11,6 @@ LOCAL_SHARED_LIBRARIES := \
libcutils \
LOCAL_MODULE:= qemud
+LOCAL_MODULE_TAGS := debug
include $(BUILD_EXECUTABLE)
diff --git a/emulator/qemud/qemud.c b/emulator/qemud/qemud.c
index ae6797e21..bd49e5253 100644
--- a/emulator/qemud/qemud.c
+++ b/emulator/qemud/qemud.c
@@ -10,96 +10,79 @@
#include <cutils/sockets.h>
/*
- * the qemud program is only used within the Android emulator as a bridge
+ * the qemud daemon program is only used within Android as a bridge
* between the emulator program and the emulated system. it really works as
* a simple stream multiplexer that works as follows:
*
+ * - qemud is started by init following instructions in
+ * /system/etc/init.goldfish.rc (i.e. it is never started on real devices)
+ *
* - qemud communicates with the emulator program through a single serial
* port, whose name is passed through a kernel boot parameter
* (e.g. android.qemud=ttyS1)
*
- * - qemud setups one or more unix local stream sockets in the
- * emulated system each one of these represent a different communication
- * 'channel' between the emulator program and the emulated system.
- *
- * as an example, one channel is used for the emulated GSM modem
- * (AT command channel), another channel is used for the emulated GPS,
- * etc...
- *
- * - the protocol used on the serial connection is pretty simple:
- *
- * offset size description
- * 0 4 4-char hex string giving the payload size
- * 4 2 2-char hex string giving the destination or
- * source channel
- * 6 n the message payload
+ * - qemud binds one unix local stream socket (/dev/socket/qemud, created
+ * by init through /system/etc/init.goldfish.rc).
*
- * for emulator->system messages, the 'channel' index indicates
- * to which channel the payload must be sent
*
- * for system->emulator messages, the 'channel' index indicates from
- * which channel the payload comes from.
+ * emulator <==serial==> qemud <---> /dev/socket/qemud <-+--> client1
+ * |
+ * +--> client2
*
- * - a special channel index (0) is used to communicate with the qemud
- * program directly from the emulator. this is used for the following
- * commands: (content of the payload):
+ * - the special channel index 0 is used by the emulator and qemud only.
+ * other channel numbers correspond to clients. More specifically,
+ * connection are created like this:
*
- * request: connect:<name>
- * answer: ok:connect:<name>:XX // succesful name lookup
- * answer: ko:connect:bad name // failed lookup
+ * * the client connects to /dev/socket/qemud
*
- * the emulator queries the index of a given channel given
- * its human-readable name. the answer contains a 2-char hex
- * string for the channel index.
+ * * the client sends the service name through the socket, as
+ * <service-name>
*
- * not all emulated systems may need the same communication
- * channels, so this function may fail.
+ * * qemud creates a "Client" object internally, assigns it an
+ * internal unique channel number > 0, then sends a connection
+ * initiation request to the emulator (i.e. through channel 0):
*
- * any invalid request will get an answer of:
- *
- * ko:unknown command
+ * connect:<hxid>:<name>
*
+ * where <name> is the service name, and <hxid> is a 4-hexchar
+ * number corresponding to the channel number.
*
- * here's a diagram of how things work:
+ * * in case of success, the emulator responds through channel 0
+ * with:
*
+ * ok:connect:<hxid>
*
- * _________
- * _____________ creates | |
- * ________ | |==========>| Channel |--*--
- * | |---->| Multiplexer | |_________|
- * --*--| Serial | |_____________| || creates
- * |________| | _____v___
- * A +--------------->| |
- * | | Client |--*--
- * +---------------------------------|_________|
+ * after this, all messages between the client and the emulator
+ * are passed in pass-through mode.
*
- * which really means that:
+ * * if the emulator refuses the service connection, it will
+ * send the following through channel 0:
*
- * - the multiplexer creates one Channel object per control socket qemud
- * handles (e.g. /dev/socket/qemud_gsm, /dev/socket/qemud_gps)
+ * ko:connect:<hxid>:reason-for-failure
*
- * - each Channel object has a numerical index that is >= 1, and waits
- * for client connection. it will create a Client object when this
- * happens
+ * * If the client closes the connection, qemud sends the following
+ * to the emulator:
*
- * - the Serial object receives packets from the serial port and sends them
- * to the multiplexer
+ * disconnect:<hxid>
*
- * - the multiplexer tries to find a channel the packet is addressed to,
- * and will send the packet to all clients that correspond to it
+ * The same message is the opposite direction if the emulator
+ * chooses to close the connection.
*
- * - when a Client receives data, it sends it directly to the Serial object
+ * * any command sent through channel 0 to the emulator that is
+ * not properly recognized will be answered by:
*
- * - there are two kinds of Channel objects:
+ * ko:unknown command
*
- * CHANNEL_BROADCAST :: used for emulator -> clients broadcasts only
*
- * CHANNEL_DUPLEX :: used for bidirectional communication with the
- * emulator, with only *one* client allowed per
- * duplex channel
+ * Internally, the daemon maintains a "Client" object for each client
+ * connection (i.e. accepting socket connection).
*/
-#define DEBUG 0
+/* name of the single control socket used by the daemon */
+#define CONTROL_SOCKET_NAME "qemud"
+
+#define DEBUG 1
+#define T_ACTIVE 0 /* set to 1 to dump traffic */
#if DEBUG
# define LOG_TAG "qemud"
@@ -107,6 +90,13 @@
# define D(...) LOGD(__VA_ARGS__)
#else
# define D(...) ((void)0)
+# define T(...) ((void)0)
+#endif
+
+#if T_ACTIVE
+# define T(...) D(__VA_ARGS__)
+#else
+# define T(...) ((void)0)
#endif
/** UTILITIES
@@ -262,30 +252,84 @@ fd_setnonblock(int fd)
}
}
+
+static int
+fd_accept(int fd)
+{
+ struct sockaddr from;
+ socklen_t fromlen = sizeof(from);
+ int ret;
+
+ do {
+ ret = accept(fd, &from, &fromlen);
+ } while (ret < 0 && errno == EINTR);
+
+ return ret;
+}
+
/** FD EVENT LOOP
**/
+/* A Looper object is used to monitor activity on one or more
+ * file descriptors (e.g sockets).
+ *
+ * - call looper_add() to register a function that will be
+ * called when events happen on the file descriptor.
+ *
+ * - call looper_enable() or looper_disable() to enable/disable
+ * the set of monitored events for a given file descriptor.
+ *
+ * - call looper_del() to unregister a file descriptor.
+ * this does *not* close the file descriptor.
+ *
+ * Note that you can only provide a single function to handle
+ * all events related to a given file descriptor.
+
+ * You can call looper_enable/_disable/_del within a function
+ * callback.
+ */
+
+/* the current implementation uses Linux's epoll facility
+ * the event mask we use are simply combinations of EPOLLIN
+ * EPOLLOUT, EPOLLHUP and EPOLLERR
+ */
#include <sys/epoll.h>
#define MAX_CHANNELS 16
#define MAX_EVENTS (MAX_CHANNELS+1) /* each channel + the serial fd */
+/* the event handler function type, 'user' is a user-specific
+ * opaque pointer passed to looper_add().
+ */
typedef void (*EventFunc)( void* user, int events );
+/* bit flags for the LoopHook structure.
+ *
+ * HOOK_PENDING means that an event happened on the
+ * corresponding file descriptor.
+ *
+ * HOOK_CLOSING is used to delay-close monitored
+ * file descriptors.
+ */
enum {
HOOK_PENDING = (1 << 0),
HOOK_CLOSING = (1 << 1),
};
+/* A LoopHook structure is used to monitor a given
+ * file descriptor and record its event handler.
+ */
typedef struct {
int fd;
- int wanted;
- int events;
- int state;
- void* ev_user;
- EventFunc ev_func;
+ int wanted; /* events we are monitoring */
+ int events; /* events that occured */
+ int state; /* see HOOK_XXX constants */
+ void* ev_user; /* user-provided handler parameter */
+ EventFunc ev_func; /* event handler callback */
} LoopHook;
+/* Looper is the main object modeling a looper object
+ */
typedef struct {
int epoll_fd;
int num_fds;
@@ -294,6 +338,7 @@ typedef struct {
LoopHook* hooks;
} Looper;
+/* initialize a looper object */
static void
looper_init( Looper* l )
{
@@ -304,6 +349,7 @@ looper_init( Looper* l )
l->hooks = NULL;
}
+/* finalize a looper object */
static void
looper_done( Looper* l )
{
@@ -316,6 +362,9 @@ looper_done( Looper* l )
l->epoll_fd = -1;
}
+/* return the LoopHook corresponding to a given
+ * monitored file descriptor, or NULL if not found
+ */
static LoopHook*
looper_find( Looper* l, int fd )
{
@@ -329,6 +378,7 @@ looper_find( Looper* l, int fd )
return NULL;
}
+/* grow the arrays in the looper object */
static void
looper_grow( Looper* l )
{
@@ -351,6 +401,9 @@ looper_grow( Looper* l )
}
}
+/* register a file descriptor and its event handler.
+ * no event mask will be enabled
+ */
static void
looper_add( Looper* l, int fd, EventFunc func, void* user )
{
@@ -378,6 +431,8 @@ looper_add( Looper* l, int fd, EventFunc func, void* user )
l->num_fds += 1;
}
+/* unregister a file descriptor and its event handler
+ */
static void
looper_del( Looper* l, int fd )
{
@@ -393,6 +448,10 @@ looper_del( Looper* l, int fd )
epoll_ctl( l->epoll_fd, EPOLL_CTL_DEL, fd, NULL );
}
+/* enable monitoring of certain events for a file
+ * descriptor. This adds 'events' to the current
+ * event mask
+ */
static void
looper_enable( Looper* l, int fd, int events )
{
@@ -414,6 +473,10 @@ looper_enable( Looper* l, int fd, int events )
}
}
+/* disable monitoring of certain events for a file
+ * descriptor. This ignores events that are not
+ * currently enabled.
+ */
static void
looper_disable( Looper* l, int fd, int events )
{
@@ -435,6 +498,9 @@ looper_disable( Looper* l, int fd, int events )
}
}
+/* wait until an event occurs on one of the registered file
+ * descriptors. Only returns in case of error !!
+ */
static void
looper_loop( Looper* l )
{
@@ -450,6 +516,11 @@ looper_loop( Looper* l )
return;
}
+ if (count == 0) {
+ D("%s: huh ? epoll returned count=0", __FUNCTION__);
+ continue;
+ }
+
/* mark all pending hooks */
for (n = 0; n < count; n++) {
LoopHook* hook = l->events[n].data.ptr;
@@ -483,13 +554,87 @@ looper_loop( Looper* l )
}
}
+#if T_ACTIVE
+char*
+quote( const void* data, int len )
+{
+ const char* p = data;
+ const char* end = p + len;
+ int count = 0;
+ int phase = 0;
+ static char* buff = NULL;
+
+ for (phase = 0; phase < 2; phase++) {
+ if (phase != 0) {
+ xfree(buff);
+ buff = xalloc(count+1);
+ }
+ count = 0;
+ for (p = data; p < end; p++) {
+ int c = *p;
+
+ if (c == '\\') {
+ if (phase != 0) {
+ buff[count] = buff[count+1] = '\\';
+ }
+ count += 2;
+ continue;
+ }
+
+ if (c >= 32 && c < 127) {
+ if (phase != 0)
+ buff[count] = c;
+ count += 1;
+ continue;
+ }
+
+
+ if (c == '\t') {
+ if (phase != 0) {
+ memcpy(buff+count, "<TAB>", 5);
+ }
+ count += 5;
+ continue;
+ }
+ if (c == '\n') {
+ if (phase != 0) {
+ memcpy(buff+count, "<LN>", 4);
+ }
+ count += 4;
+ continue;
+ }
+ if (c == '\r') {
+ if (phase != 0) {
+ memcpy(buff+count, "<CR>", 4);
+ }
+ count += 4;
+ continue;
+ }
+
+ if (phase != 0) {
+ buff[count+0] = '\\';
+ buff[count+1] = 'x';
+ buff[count+2] = "0123456789abcdef"[(c >> 4) & 15];
+ buff[count+3] = "0123456789abcdef"[ (c) & 15];
+ }
+ count += 4;
+ }
+ }
+ buff[count] = 0;
+ return buff;
+}
+#endif /* T_ACTIVE */
+
/** PACKETS
+ **
+ ** We need a way to buffer data before it can be sent to the
+ ** corresponding file descriptor. We use linked list of Packet
+ ** objects to do this.
**/
typedef struct Packet Packet;
-/* we want to ensure that Packet is no more than a single page */
-#define MAX_PAYLOAD (4096-16-6)
+#define MAX_PAYLOAD 4000
struct Packet {
Packet* next;
@@ -498,8 +643,13 @@ struct Packet {
uint8_t data[ MAX_PAYLOAD ];
};
+/* we expect to alloc/free a lot of packets during
+ * operations so use a single linked list of free packets
+ * to keep things speedy and simple.
+ */
static Packet* _free_packets;
+/* Allocate a packet */
static Packet*
packet_alloc(void)
{
@@ -509,12 +659,16 @@ packet_alloc(void)
} else {
xnew(p);
}
- p->next = NULL;
- p->len = 0;
+ p->next = NULL;
+ p->len = 0;
p->channel = -1;
return p;
}
+/* Release a packet. This takes the address of a packet
+ * pointer that will be set to NULL on exit (avoids
+ * referencing dangling pointers in case of bugs)
+ */
static void
packet_free( Packet* *ppacket )
{
@@ -526,18 +680,15 @@ packet_free( Packet* *ppacket )
}
}
-static Packet*
-packet_dup( Packet* p )
-{
- Packet* p2 = packet_alloc();
-
- p2->len = p->len;
- p2->channel = p->channel;
- memcpy(p2->data, p->data, p->len);
- return p2;
-}
-
/** PACKET RECEIVER
+ **
+ ** Simple abstraction for something that can receive a packet
+ ** from a FDHandler (see below) or something else.
+ **
+ ** Send a packet to it with 'receiver_post'
+ **
+ ** Call 'receiver_close' to indicate that the corresponding
+ ** packet source was closed.
**/
typedef void (*PostFunc) ( void* user, Packet* p );
@@ -549,41 +700,130 @@ typedef struct {
void* user;
} Receiver;
+/* post a packet to a receiver. Note that this transfers
+ * ownership of the packet to the receiver.
+ */
static __inline__ void
receiver_post( Receiver* r, Packet* p )
{
- r->post( r->user, p );
+ if (r->post)
+ r->post( r->user, p );
+ else
+ packet_free(&p);
}
+/* tell a receiver the packet source was closed.
+ * this will also prevent further posting to the
+ * receiver.
+ */
static __inline__ void
receiver_close( Receiver* r )
{
- r->close( r->user );
+ if (r->close) {
+ r->close( r->user );
+ r->close = NULL;
+ }
+ r->post = NULL;
}
/** FD HANDLERS
**
** these are smart listeners that send incoming packets to a receiver
- ** and can queue one or more outgoing packets and send them when possible
+ ** and can queue one or more outgoing packets and send them when
+ ** possible to the FD.
+ **
+ ** note that we support clean shutdown of file descriptors,
+ ** i.e. we try to send all outgoing packets before destroying
+ ** the FDHandler.
**/
-typedef struct FDHandler {
- int fd;
+typedef struct FDHandler FDHandler;
+typedef struct FDHandlerList FDHandlerList;
+
+struct FDHandler {
+ int fd;
+ FDHandlerList* list;
+ char closing;
+ Receiver receiver[1];
+
+ /* queue of outgoing packets */
+ int out_pos;
+ Packet* out_first;
+ Packet** out_ptail;
+
+ FDHandler* next;
+ FDHandler** pref;
+
+};
+
+struct FDHandlerList {
+ /* the looper that manages the fds */
Looper* looper;
- Receiver receiver[1];
- int out_pos;
- Packet* out_first;
- Packet** out_ptail;
-} FDHandler;
+ /* list of active FDHandler objects */
+ FDHandler* active;
+ /* list of closing FDHandler objects.
+ * these are waiting to push their
+ * queued packets to the fd before
+ * freeing themselves.
+ */
+ FDHandler* closing;
+};
+
+/* remove a FDHandler from its current list */
static void
-fdhandler_done( FDHandler* f )
+fdhandler_remove( FDHandler* f )
{
- /* get rid of unsent packets */
- if (f->out_first) {
+ f->pref[0] = f->next;
+ if (f->next)
+ f->next->pref = f->pref;
+}
+
+/* add a FDHandler to a given list */
+static void
+fdhandler_prepend( FDHandler* f, FDHandler** list )
+{
+ f->next = list[0];
+ f->pref = list;
+ list[0] = f;
+ if (f->next)
+ f->next->pref = &f->next;
+}
+
+/* initialize a FDHandler list */
+static void
+fdhandler_list_init( FDHandlerList* list, Looper* looper )
+{
+ list->looper = looper;
+ list->active = NULL;
+ list->closing = NULL;
+}
+
+
+/* close a FDHandler (and free it). Note that this will not
+ * perform a graceful shutdown, i.e. all packets in the
+ * outgoing queue will be immediately free.
+ *
+ * this *will* notify the receiver that the file descriptor
+ * was closed.
+ *
+ * you should call fdhandler_shutdown() if you want to
+ * notify the FDHandler that its packet source is closed.
+ */
+static void
+fdhandler_close( FDHandler* f )
+{
+ /* notify receiver */
+ receiver_close(f->receiver);
+
+ /* remove the handler from its list */
+ fdhandler_remove(f);
+
+ /* get rid of outgoing packet queue */
+ if (f->out_first != NULL) {
Packet* p;
while ((p = f->out_first) != NULL) {
f->out_first = p->next;
@@ -593,14 +833,41 @@ fdhandler_done( FDHandler* f )
/* get rid of file descriptor */
if (f->fd >= 0) {
- looper_del( f->looper, f->fd );
+ looper_del( f->list->looper, f->fd );
close(f->fd);
f->fd = -1;
}
- f->looper = NULL;
+
+ f->list = NULL;
+ xfree(f);
}
+/* Ask the FDHandler to cleanly shutdown the connection,
+ * i.e. send any pending outgoing packets then auto-free
+ * itself.
+ */
+static void
+fdhandler_shutdown( FDHandler* f )
+{
+
+ if (f->out_first != NULL && !f->closing)
+ {
+ /* move the handler to the 'closing' list */
+ f->closing = 1;
+ fdhandler_remove(f);
+ fdhandler_prepend(f, &f->list->closing);
+ /* notify the receiver that we're closing */
+ receiver_close(f->receiver);
+ return;
+ }
+
+ fdhandler_close(f);
+}
+
+/* Enqueue a new packet that the FDHandler will
+ * send through its file descriptor.
+ */
static void
fdhandler_enqueue( FDHandler* f, Packet* p )
{
@@ -612,16 +879,24 @@ fdhandler_enqueue( FDHandler* f, Packet* p )
if (first == NULL) {
f->out_pos = 0;
- looper_enable( f->looper, f->fd, EPOLLOUT );
+ looper_enable( f->list->looper, f->fd, EPOLLOUT );
}
}
+/* FDHandler file descriptor event callback for read/write ops */
static void
fdhandler_event( FDHandler* f, int events )
{
int len;
+ /* in certain cases, it's possible to have both EPOLLIN and
+ * EPOLLHUP at the same time. This indicates that there is incoming
+ * data to read, but that the connection was nonetheless closed
+ * by the sender. Be sure to read the data before closing
+ * the receiver to avoid packet loss.
+ */
+
if (events & EPOLLIN) {
Packet* p = packet_alloc();
int len;
@@ -629,23 +904,17 @@ fdhandler_event( FDHandler* f, int events )
if ((len = fd_read(f->fd, p->data, MAX_PAYLOAD)) < 0) {
D("%s: can't recv: %s", __FUNCTION__, strerror(errno));
packet_free(&p);
- } else {
+ } else if (len > 0) {
p->len = len;
- p->channel = -101; /* special debug value */
+ p->channel = -101; /* special debug value, not used */
receiver_post( f->receiver, p );
}
}
- /* in certain cases, it's possible to have both EPOLLIN and
- * EPOLLHUP at the same time. This indicates that there is incoming
- * data to read, but that the connection was nonetheless closed
- * by the sender. Be sure to read the data before closing
- * the receiver to avoid packet loss.
- */
if (events & (EPOLLHUP|EPOLLERR)) {
/* disconnection */
D("%s: disconnect on fd %d", __FUNCTION__, f->fd);
- receiver_close( f->receiver );
+ fdhandler_close(f);
return;
}
@@ -664,7 +933,7 @@ fdhandler_event( FDHandler* f, int events )
packet_free(&p);
if (f->out_first == NULL) {
f->out_ptail = &f->out_first;
- looper_disable( f->looper, f->fd, EPOLLOUT );
+ looper_disable( f->list->looper, f->fd, EPOLLOUT );
}
}
}
@@ -672,24 +941,34 @@ fdhandler_event( FDHandler* f, int events )
}
-static void
-fdhandler_init( FDHandler* f,
- int fd,
- Looper* looper,
- Receiver* receiver )
+/* Create a new FDHandler that monitors read/writes */
+static FDHandler*
+fdhandler_new( int fd,
+ FDHandlerList* list,
+ Receiver* receiver )
{
+ FDHandler* f = xalloc0(sizeof(*f));
+
f->fd = fd;
- f->looper = looper;
+ f->list = list;
f->receiver[0] = receiver[0];
f->out_first = NULL;
f->out_ptail = &f->out_first;
f->out_pos = 0;
- looper_add( looper, fd, (EventFunc) fdhandler_event, f );
- looper_enable( looper, fd, EPOLLIN );
+ fdhandler_prepend(f, &list->active);
+
+ looper_add( list->looper, fd, (EventFunc) fdhandler_event, f );
+ looper_enable( list->looper, fd, EPOLLIN );
+
+ return f;
}
+/* event callback function to monitor accepts() on server sockets.
+ * the convention used here is that the receiver will receive a
+ * dummy packet with the new client socket in p->channel
+ */
static void
fdhandler_accept_event( FDHandler* f, int events )
{
@@ -700,296 +979,116 @@ fdhandler_accept_event( FDHandler* f, int events )
D("%s: accepting on fd %d", __FUNCTION__, f->fd);
p->data[0] = 1;
p->len = 1;
+ p->channel = fd_accept(f->fd);
+ if (p->channel < 0) {
+ D("%s: accept failed ?: %s", __FUNCTION__, strerror(errno));
+ packet_free(&p);
+ return;
+ }
receiver_post( f->receiver, p );
}
if (events & (EPOLLHUP|EPOLLERR)) {
/* disconnecting !! */
- D("%s: closing fd %d", __FUNCTION__, f->fd);
- receiver_close( f->receiver );
+ D("%s: closing accept fd %d", __FUNCTION__, f->fd);
+ fdhandler_close(f);
return;
}
}
-static void
-fdhandler_init_accept( FDHandler* f,
- int fd,
- Looper* looper,
- Receiver* receiver )
+/* Create a new FDHandler used to monitor new connections on a
+ * server socket. The receiver must expect the new connection
+ * fd in the 'channel' field of a dummy packet.
+ */
+static FDHandler*
+fdhandler_new_accept( int fd,
+ FDHandlerList* list,
+ Receiver* receiver )
{
+ FDHandler* f = xalloc0(sizeof(*f));
+
f->fd = fd;
- f->looper = looper;
+ f->list = list;
f->receiver[0] = receiver[0];
- looper_add( looper, fd, (EventFunc) fdhandler_accept_event, f );
- looper_enable( looper, fd, EPOLLIN );
-}
+ fdhandler_prepend(f, &list->active);
-/** CLIENTS
- **/
-
-typedef struct Client {
- struct Client* next;
- struct Client** pref;
- int channel;
- FDHandler fdhandler[1];
- Receiver receiver[1];
-} Client;
-
-static Client* _free_clients;
-
-static void
-client_free( Client* c )
-{
- c->pref[0] = c->next;
- c->next = NULL;
- c->pref = &c->next;
-
- fdhandler_done( c->fdhandler );
- free(c);
-}
-
-static void
-client_receive( Client* c, Packet* p )
-{
- p->channel = c->channel;
- receiver_post( c->receiver, p );
-}
-
-static void
-client_send( Client* c, Packet* p )
-{
- fdhandler_enqueue( c->fdhandler, p );
-}
-
-static void
-client_close( Client* c )
-{
- D("disconnecting client on fd %d", c->fdhandler->fd);
- client_free(c);
-}
-
-static Client*
-client_new( int fd,
- int channel,
- Looper* looper,
- Receiver* receiver )
-{
- Client* c;
- Receiver recv;
-
- xnew(c);
-
- c->next = NULL;
- c->pref = &c->next;
- c->channel = channel;
- c->receiver[0] = receiver[0];
-
- recv.user = c;
- recv.post = (PostFunc) client_receive;
- recv.close = (CloseFunc) client_close;
-
- fdhandler_init( c->fdhandler, fd, looper, &recv );
- return c;
-}
-
-static void
-client_link( Client* c, Client** plist )
-{
- c->next = plist[0];
- c->pref = plist;
- plist[0] = c;
-}
-
-
-/** CHANNELS
- **/
-
-typedef enum {
- CHANNEL_BROADCAST = 0,
- CHANNEL_DUPLEX,
-
- CHANNEL_MAX /* do not remove */
-
-} ChannelType;
-
-#define CHANNEL_CONTROL 0
-
-typedef struct Channel {
- struct Channel* next;
- struct Channel** pref;
- FDHandler fdhandler[1];
- ChannelType ctype;
- const char* name;
- int index;
- Receiver receiver[1];
- Client* clients;
-} Channel;
-
-static void
-channel_free( Channel* c )
-{
- while (c->clients)
- client_free(c->clients);
-
- c->pref[0] = c->next;
- c->pref = &c->next;
- c->next = NULL;
-
- fdhandler_done( c->fdhandler );
- free(c);
-}
-
-static void
-channel_close( Channel* c )
-{
- D("closing channel '%s' on fd %d", c->name, c->fdhandler->fd);
- channel_free(c);
-}
-
-
-static void
-channel_accept( Channel* c, Packet* p )
-{
- int fd;
- struct sockaddr from;
- socklen_t fromlen = sizeof(from);
-
- /* get rid of dummy packet (see fdhandler_event_accept) */
- packet_free(&p);
-
- do {
- fd = accept( c->fdhandler->fd, &from, &fromlen );
- } while (fd < 0 && errno == EINTR);
-
- if (fd >= 0) {
- Client* client;
-
- /* DUPLEX channels can only have one client at a time */
- if (c->ctype == CHANNEL_DUPLEX && c->clients != NULL) {
- D("refusing client connection on duplex channel '%s'", c->name);
- close(fd);
- return;
- }
- client = client_new( fd, c->index, c->fdhandler->looper, c->receiver );
- client_link( client, &c->clients );
- D("new client for channel '%s' on fd %d", c->name, fd);
- }
- else
- D("could not accept connection: %s", strerror(errno));
-}
-
-
-static Channel*
-channel_new( int fd,
- ChannelType ctype,
- const char* name,
- int index,
- Looper* looper,
- Receiver* receiver )
-{
- Channel* c;
- Receiver recv;
-
- xnew(c);
-
- c->next = NULL;
- c->pref = &c->next;
- c->ctype = ctype;
- c->name = name;
- c->index = index;
-
- /* saved for future clients */
- c->receiver[0] = receiver[0];
-
- recv.user = c;
- recv.post = (PostFunc) channel_accept;
- recv.close = (CloseFunc) channel_close;
-
- fdhandler_init_accept( c->fdhandler, fd, looper, &recv );
+ looper_add( list->looper, fd, (EventFunc) fdhandler_accept_event, f );
+ looper_enable( list->looper, fd, EPOLLIN );
listen( fd, 5 );
- return c;
-}
-
-static void
-channel_link( Channel* c, Channel** plist )
-{
- c->next = plist[0];
- c->pref = plist;
- plist[0] = c;
-}
-
-static void
-channel_send( Channel* c, Packet* p )
-{
- Client* client = c->clients;
- for ( ; client; client = client->next ) {
- Packet* q = packet_dup(p);
- client_send( client, q );
- }
- packet_free( &p );
+ return f;
}
+/** SERIAL CONNECTION STATE
+ **
+ ** The following is used to handle the framing protocol
+ ** used on the serial port connection.
+ **/
/* each packet is made of a 6 byte header followed by a payload
* the header looks like:
*
* offset size description
- * 0 4 a 4-char hex string for the size of the payload
- * 4 2 a 2-byte hex string for the channel number
+ * 0 2 a 2-byte hex string for the channel number
+ * 4 4 a 4-char hex string for the size of the payload
* 6 n the payload itself
*/
#define HEADER_SIZE 6
-#define LENGTH_OFFSET 0
-#define LENGTH_SIZE 4
-#define CHANNEL_OFFSET 4
+#define CHANNEL_OFFSET 0
+#define LENGTH_OFFSET 2
#define CHANNEL_SIZE 2
+#define LENGTH_SIZE 4
-#define CHANNEL_INDEX_NONE 0
-#define CHANNEL_INDEX_CONTROL 1
-
-#define TOSTRING(x) _TOSTRING(x)
-#define _TOSTRING(x) #x
-
-/** SERIAL HANDLER
- **/
+#define CHANNEL_CONTROL 0
+/* The Serial object receives data from the serial port,
+ * extracts the payload size and channel index, then sends
+ * the resulting messages as a packet to a generic receiver.
+ *
+ * You can also use serial_send to send a packet through
+ * the serial port.
+ */
typedef struct Serial {
- FDHandler fdhandler[1];
- Receiver receiver[1];
- int in_len;
- int in_datalen;
- int in_channel;
- Packet* in_packet;
+ FDHandler* fdhandler; /* used to monitor serial port fd */
+ Receiver receiver[1]; /* send payload there */
+ int in_len; /* current bytes in input packet */
+ int in_datalen; /* payload size, or 0 when reading header */
+ int in_channel; /* extracted channel number */
+ Packet* in_packet; /* used to read incoming packets */
} Serial;
+
+/* a callback called when the serial port's fd is closed */
static void
-serial_done( Serial* s )
+serial_fd_close( Serial* s )
{
- packet_free(&s->in_packet);
- s->in_len = 0;
- s->in_datalen = 0;
- s->in_channel = 0;
- fdhandler_done(s->fdhandler);
+ fatal("unexpected serial port close !!");
}
static void
-serial_close( Serial* s )
+serial_dump( Packet* p, const char* funcname )
{
- fatal("unexpected serial port close !!");
+ T("%s: %03d bytes: '%s'",
+ funcname, p->len, quote(p->data, p->len));
}
-/* receive packets from the serial port */
+/* a callback called when a packet arrives from the serial port's FDHandler.
+ *
+ * This will essentially parse the header, extract the channel number and
+ * the payload size and store them in 'in_datalen' and 'in_channel'.
+ *
+ * After that, the payload is sent to the receiver once completed.
+ */
static void
-serial_receive( Serial* s, Packet* p )
+serial_fd_receive( Serial* s, Packet* p )
{
int rpos = 0, rcount = p->len;
Packet* inp = s->in_packet;
int inpos = s->in_len;
- //D("received from serial: %d bytes: '%.*s'", p->len, p->len, p->data);
+ serial_dump( p, __FUNCTION__ );
while (rpos < rcount)
{
@@ -1009,8 +1108,11 @@ serial_receive( Serial* s, Packet* p )
s->in_datalen = hex2int( inp->data + LENGTH_OFFSET, LENGTH_SIZE );
s->in_channel = hex2int( inp->data + CHANNEL_OFFSET, CHANNEL_SIZE );
- if (s->in_datalen <= 0)
- D("ignoring empty packet from serial port");
+ if (s->in_datalen <= 0) {
+ D("ignoring %s packet from serial port",
+ s->in_datalen ? "empty" : "malformed");
+ s->in_datalen = 0;
+ }
//D("received %d bytes packet for channel %d", s->in_datalen, s->in_channel);
inpos = 0;
@@ -1047,7 +1149,10 @@ serial_receive( Serial* s, Packet* p )
}
-/* send a packet to the serial port */
+/* send a packet to the serial port.
+ * this assumes that p->len and p->channel contain the payload's
+ * size and channel and will add the appropriate header.
+ */
static void
serial_send( Serial* s, Packet* p )
{
@@ -1060,201 +1165,486 @@ serial_send( Serial* s, Packet* p )
int2hex( p->len, h->data + LENGTH_OFFSET, LENGTH_SIZE );
int2hex( p->channel, h->data + CHANNEL_OFFSET, CHANNEL_SIZE );
+ serial_dump( h, __FUNCTION__ );
+ serial_dump( p, __FUNCTION__ );
+
fdhandler_enqueue( s->fdhandler, h );
fdhandler_enqueue( s->fdhandler, p );
}
+/* initialize serial reader */
static void
-serial_init( Serial* s,
- int fd,
- Looper* looper,
- Receiver* receiver )
+serial_init( Serial* s,
+ int fd,
+ FDHandlerList* list,
+ Receiver* receiver )
{
Receiver recv;
recv.user = s;
- recv.post = (PostFunc) serial_receive;
- recv.close = (CloseFunc) serial_close;
+ recv.post = (PostFunc) serial_fd_receive;
+ recv.close = (CloseFunc) serial_fd_close;
s->receiver[0] = receiver[0];
- fdhandler_init( s->fdhandler, fd, looper, &recv );
+ s->fdhandler = fdhandler_new( fd, list, &recv );
s->in_len = 0;
s->in_datalen = 0;
s->in_channel = 0;
s->in_packet = packet_alloc();
}
-/** GLOBAL MULTIPLEXER
+
+/** CLIENTS
**/
-typedef struct {
- Looper looper[1];
- Serial serial[1];
- Channel* channels;
- uint16_t channel_last;
-} Multiplexer;
+typedef struct Client Client;
+typedef struct Multiplexer Multiplexer;
+
+/* A Client object models a single qemud client socket
+ * connection in the emulated system.
+ *
+ * the client first sends the name of the system service
+ * it wants to contact (no framing), then waits for a 2
+ * byte answer from qemud.
+ *
+ * the answer is either "OK" or "KO" to indicate
+ * success or failure.
+ *
+ * In case of success, the client can send messages
+ * to the service.
+ *
+ * In case of failure, it can disconnect or try sending
+ * the name of another service.
+ */
+struct Client {
+ Client* next;
+ Client** pref;
+ int channel;
+ char registered;
+ FDHandler* fdhandler;
+ Multiplexer* multiplexer;
+};
+
+struct Multiplexer {
+ Client* clients;
+ int last_channel;
+ Serial serial[1];
+ Looper looper[1];
+ FDHandlerList fdhandlers[1];
+};
-/* receive a packet from the serial port, send it to the relevant client/channel */
-static void multiplexer_receive_serial( Multiplexer* m, Packet* p );
+
+static int multiplexer_open_channel( Multiplexer* mult, Packet* p );
+static void multiplexer_close_channel( Multiplexer* mult, int channel );
+static void multiplexer_serial_send( Multiplexer* mult, int channel, Packet* p );
static void
-multiplexer_init( Multiplexer* m, const char* serial_dev )
+client_dump( Client* c, Packet* p, const char* funcname )
{
- int fd;
- Receiver recv;
+ T("%s: client %p (%d): %3d bytes: '%s'",
+ funcname, c, c->fdhandler->fd,
+ p->len, quote(p->data, p->len));
+}
- looper_init( m->looper );
+/* destroy a client */
+static void
+client_free( Client* c )
+{
+ /* remove from list */
+ c->pref[0] = c->next;
+ if (c->next)
+ c->next->pref = c->pref;
- fd = open(serial_dev, O_RDWR);
- if (fd < 0) {
- fatal( "%s: could not open '%s': %s", __FUNCTION__, serial_dev,
- strerror(errno) );
+ c->channel = -1;
+ c->registered = 0;
+
+ /* gently ask the FDHandler to shutdown to
+ * avoid losing queued outgoing packets */
+ if (c->fdhandler != NULL) {
+ fdhandler_shutdown(c->fdhandler);
+ c->fdhandler = NULL;
}
- // disable echo on serial lines
- if ( !memcmp( serial_dev, "/dev/ttyS", 9 ) ) {
- struct termios ios;
- tcgetattr( fd, &ios );
- ios.c_lflag = 0; /* disable ECHO, ICANON, etc... */
- tcsetattr( fd, TCSANOW, &ios );
+
+ xfree(c);
+}
+
+
+/* a function called when a client socket receives data */
+static void
+client_fd_receive( Client* c, Packet* p )
+{
+ client_dump(c, p, __FUNCTION__);
+
+ if (c->registered) {
+ /* the client is registered, just send the
+ * data through the serial port
+ */
+ multiplexer_serial_send(c->multiplexer, c->channel, p);
+ return;
}
- recv.user = m;
- recv.post = (PostFunc) multiplexer_receive_serial;
- recv.close = NULL;
+ if (c->channel > 0) {
+ /* the client is waiting registration results.
+ * this should not happen because the client
+ * should wait for our 'ok' or 'ko'.
+ * close the connection.
+ */
+ D("%s: bad client sending data before end of registration",
+ __FUNCTION__);
+ BAD_CLIENT:
+ packet_free(&p);
+ client_free(c);
+ return;
+ }
- serial_init( m->serial, fd, m->looper, &recv );
+ /* the client hasn't registered a service yet,
+ * so this must be the name of a service, call
+ * the multiplexer to start registration for
+ * it.
+ */
+ D("%s: attempting registration for service '%.*s'",
+ __FUNCTION__, p->len, p->data);
+ c->channel = multiplexer_open_channel(c->multiplexer, p);
+ if (c->channel < 0) {
+ D("%s: service name too long", __FUNCTION__);
+ goto BAD_CLIENT;
+ }
+ D("%s: -> received channel id %d", __FUNCTION__, c->channel);
+ packet_free(&p);
+}
+
+
+/* a function called when the client socket is closed. */
+static void
+client_fd_close( Client* c )
+{
+ T("%s: client %p (%d)", __FUNCTION__, c, c->fdhandler->fd);
+
+ /* no need to shutdown the FDHandler */
+ c->fdhandler = NULL;
+
+ /* tell the emulator we're out */
+ if (c->channel > 0)
+ multiplexer_close_channel(c->multiplexer, c->channel);
+
+ /* free the client */
+ client_free(c);
+}
+
+/* a function called when the multiplexer received a registration
+ * response from the emulator for a given client.
+ */
+static void
+client_registration( Client* c, int registered )
+{
+ Packet* p = packet_alloc();
- m->channels = NULL;
- m->channel_last = CHANNEL_CONTROL+1;
+ /* sends registration status to client */
+ if (!registered) {
+ D("%s: registration failed for client %d", __FUNCTION__, c->channel);
+ memcpy( p->data, "KO", 2 );
+ p->len = 2;
+ } else {
+ D("%s: registration succeeded for client %d", __FUNCTION__, c->channel);
+ memcpy( p->data, "OK", 2 );
+ p->len = 2;
+ }
+ client_dump(c, p, __FUNCTION__);
+ fdhandler_enqueue(c->fdhandler, p);
+
+ /* now save registration state
+ */
+ c->registered = registered;
+ if (!registered) {
+ /* allow the client to try registering another service */
+ c->channel = -1;
+ }
}
+/* send data to a client */
static void
-multiplexer_add_channel( Multiplexer* m, int fd, const char* name, ChannelType ctype )
+client_send( Client* c, Packet* p )
+{
+ client_dump(c, p, __FUNCTION__);
+ fdhandler_enqueue(c->fdhandler, p);
+}
+
+
+/* Create new client socket handler */
+static Client*
+client_new( Multiplexer* mult,
+ int fd,
+ FDHandlerList* pfdhandlers,
+ Client** pclients )
{
- Channel* c;
+ Client* c;
Receiver recv;
- /* send channel client data directly to the serial port */
- recv.user = m->serial;
- recv.post = (PostFunc) serial_send;
- recv.close = (CloseFunc) client_close;
+ xnew(c);
+
+ c->multiplexer = mult;
+ c->next = NULL;
+ c->pref = &c->next;
+ c->channel = -1;
+ c->registered = 0;
- /* connect each channel directly to the serial port */
- c = channel_new( fd, ctype, name, m->channel_last, m->looper, &recv );
- channel_link( c, &m->channels );
+ recv.user = c;
+ recv.post = (PostFunc) client_fd_receive;
+ recv.close = (CloseFunc) client_fd_close;
+
+ c->fdhandler = fdhandler_new( fd, pfdhandlers, &recv );
- m->channel_last += 1;
- if (m->channel_last <= CHANNEL_CONTROL)
- m->channel_last += 1;
+ /* add to client list */
+ c->next = *pclients;
+ c->pref = pclients;
+ *pclients = c;
+ if (c->next)
+ c->next->pref = &c->next;
+
+ return c;
}
+/** GLOBAL MULTIPLEXER
+ **/
-static void
-multiplexer_done( Multiplexer* m )
+/* find a client by its channel */
+static Client*
+multiplexer_find_client( Multiplexer* mult, int channel )
{
- while (m->channels)
- channel_close(m->channels);
+ Client* c = mult->clients;
- serial_done( m->serial );
- looper_done( m->looper );
+ for ( ; c != NULL; c = c->next ) {
+ if (c->channel == channel)
+ return c;
+ }
+ return NULL;
}
+/* handle control messages coming from the serial port
+ * on CONTROL_CHANNEL.
+ */
+static void
+multiplexer_handle_control( Multiplexer* mult, Packet* p )
+{
+ /* connection registration success */
+ if (p->len == 15 && !memcmp(p->data, "ok:connect:", 11)) {
+ int channel = hex2int(p->data+11, 4);
+ Client* client = multiplexer_find_client(mult, channel);
+
+ /* note that 'client' can be NULL if the corresponding
+ * socket was closed before the emulator response arrived.
+ */
+ if (client != NULL) {
+ client_registration(client, 1);
+ }
+ goto EXIT;
+ }
+ /* connection registration failure */
+ if (p->len >= 15 && !memcmp(p->data, "ko:connect:",11)) {
+ int channel = hex2int(p->data+11, 4);
+ Client* client = multiplexer_find_client(mult, channel);
+
+ if (client != NULL)
+ client_registration(client, 0);
+
+ goto EXIT;
+ }
+
+ /* emulator-induced client disconnection */
+ if (p->len == 15 && !memcmp(p->data, "disconnect:",11)) {
+ int channel = hex2int(p->data+11, 4);
+ Client* client = multiplexer_find_client(mult, channel);
+
+ if (client != NULL)
+ client_free(client);
+
+ goto EXIT;
+ }
+
+ D("%s: unknown control message: '%.*s'",
+ __FUNCTION__, p->len, p->data);
+
+EXIT:
+ packet_free(&p);
+}
+
+/* a function called when an incoming packet comes from the serial port */
static void
-multiplexer_send_answer( Multiplexer* m, Packet* p, const char* answer )
+multiplexer_serial_receive( Multiplexer* mult, Packet* p )
{
- p->len = strlen( answer );
- if (p->len >= MAX_PAYLOAD)
- p->len = MAX_PAYLOAD-1;
+ Client* client;
- memcpy( (char*)p->data, answer, p->len );
- p->channel = CHANNEL_CONTROL;
+ if (p->channel == CHANNEL_CONTROL) {
+ multiplexer_handle_control(mult, p);
+ return;
+ }
+
+ client = multiplexer_find_client(mult, p->channel);
+ if (client != NULL) {
+ client_send(client, p);
+ return;
+ }
- serial_send( m->serial, p );
+ D("%s: discarding packet for unknown channel %d", __FUNCTION__, p->channel);
+ packet_free(&p);
}
+/* a function called when the serial reader closes */
+static void
+multiplexer_serial_close( Multiplexer* mult )
+{
+ fatal("unexpected close of serial reader");
+}
+/* a function called to send a packet to the serial port */
static void
-multiplexer_handle_connect( Multiplexer* m, Packet* p, char* name )
+multiplexer_serial_send( Multiplexer* mult, int channel, Packet* p )
{
- int n;
- Channel* c;
+ p->channel = channel;
+ serial_send( mult->serial, p );
+}
- if (p->len >= MAX_PAYLOAD) {
- multiplexer_send_answer( m, p, "ko:connect:bad name" );
- return;
+
+
+/* a function used by a client to allocate a new channel id and
+ * ask the emulator to open it. 'service' must be a packet containing
+ * the name of the service in its payload.
+ *
+ * returns -1 if the service name is too long.
+ *
+ * notice that client_registration() will be called later when
+ * the answer arrives.
+ */
+static int
+multiplexer_open_channel( Multiplexer* mult, Packet* service )
+{
+ Packet* p = packet_alloc();
+ int len, channel;
+
+ /* find a free channel number, assume we don't have many
+ * clients here. */
+ {
+ Client* c;
+ TRY_AGAIN:
+ channel = (++mult->last_channel) & 0xff;
+
+ for (c = mult->clients; c != NULL; c = c->next)
+ if (c->channel == channel)
+ goto TRY_AGAIN;
+ }
+
+ len = snprintf((char*)p->data, sizeof p->data, "connect:%.*s:%04x", service->len, service->data, channel);
+ if (len >= (int)sizeof(p->data)) {
+ D("%s: weird, service name too long (%d > %d)", __FUNCTION__, len, sizeof(p->data));
+ packet_free(&p);
+ return -1;
}
- p->data[p->len] = 0;
+ p->channel = CHANNEL_CONTROL;
+ p->len = len;
- for (c = m->channels; c != NULL; c = c->next)
- if ( !strcmp(c->name, name) )
- break;
+ serial_send(mult->serial, p);
+ return channel;
+}
- if (c == NULL) {
- D("can't connect to unknown channel '%s'", name);
- multiplexer_send_answer( m, p, "ko:connect:bad name" );
+/* used to tell the emulator a channel was closed by a client */
+static void
+multiplexer_close_channel( Multiplexer* mult, int channel )
+{
+ Packet* p = packet_alloc();
+ int len = snprintf((char*)p->data, sizeof(p->data), "disconnect:%04x", channel);
+
+ if (len > (int)sizeof(p->data)) {
+ /* should not happen */
return;
}
p->channel = CHANNEL_CONTROL;
- p->len = snprintf( (char*)p->data, MAX_PAYLOAD,
- "ok:connect:%s:%02x", c->name, c->index );
+ p->len = len;
- serial_send( m->serial, p );
+ serial_send(mult->serial, p);
}
+/* this function is used when a new connection happens on the control
+ * socket.
+ */
+static void
+multiplexer_control_accept( Multiplexer* m, Packet* p )
+{
+ /* the file descriptor for the new socket connection is
+ * in p->channel. See fdhandler_accept_event() */
+ int fd = p->channel;
+ Client* client = client_new( m, fd, m->fdhandlers, &m->clients );
+
+ D("created client %p listening on fd %d", client, fd);
+
+ /* free dummy packet */
+ packet_free(&p);
+}
static void
-multiplexer_receive_serial( Multiplexer* m, Packet* p )
+multiplexer_control_close( Multiplexer* m )
{
- Channel* c = m->channels;
+ fatal("unexpected multiplexer control close");
+}
- /* check the destination channel index */
- if (p->channel != CHANNEL_CONTROL) {
- Channel* c;
+static void
+multiplexer_init( Multiplexer* m, const char* serial_dev )
+{
+ int fd, control_fd;
+ Receiver recv;
- for (c = m->channels; c; c = c->next ) {
- if (c->index == p->channel) {
- channel_send( c, p );
- break;
- }
- }
- if (c == NULL) {
- D("ignoring %d bytes packet for unknown channel index %d",
- p->len, p->channel );
- packet_free(&p);
- }
+ /* initialize looper and fdhandlers list */
+ looper_init( m->looper );
+ fdhandler_list_init( m->fdhandlers, m->looper );
+
+ /* open the serial port */
+ do {
+ fd = open(serial_dev, O_RDWR);
+ } while (fd < 0 && errno == EINTR);
+
+ if (fd < 0) {
+ fatal( "%s: could not open '%s': %s", __FUNCTION__, serial_dev,
+ strerror(errno) );
}
- else /* packet addressed to the control channel */
- {
- D("received control message: '%.*s'", p->len, p->data);
- if (p->len > 8 && strncmp( (char*)p->data, "connect:", 8) == 0) {
- multiplexer_handle_connect( m, p, (char*)p->data + 8 );
- } else {
- /* unknown command */
- multiplexer_send_answer( m, p, "ko:unknown command" );
- }
- return;
+ // disable echo on serial lines
+ if ( !memcmp( serial_dev, "/dev/ttyS", 9 ) ) {
+ struct termios ios;
+ tcgetattr( fd, &ios );
+ ios.c_lflag = 0; /* disable ECHO, ICANON, etc... */
+ tcsetattr( fd, TCSANOW, &ios );
}
-}
+ /* initialize the serial reader/writer */
+ recv.user = m;
+ recv.post = (PostFunc) multiplexer_serial_receive;
+ recv.close = (CloseFunc) multiplexer_serial_close;
+
+ serial_init( m->serial, fd, m->fdhandlers, &recv );
+
+ /* open the qemud control socket */
+ recv.user = m;
+ recv.post = (PostFunc) multiplexer_control_accept;
+ recv.close = (CloseFunc) multiplexer_control_close;
+
+ fd = android_get_control_socket(CONTROL_SOCKET_NAME);
+ if (fd < 0) {
+ fatal("couldn't get fd for control socket '%s'", CONTROL_SOCKET_NAME);
+ }
+
+ fdhandler_new_accept( fd, m->fdhandlers, &recv );
+
+ /* initialize clients list */
+ m->clients = NULL;
+}
/** MAIN LOOP
**/
static Multiplexer _multiplexer[1];
-#define QEMUD_PREFIX "qemud_"
-
-static const struct { const char* name; ChannelType ctype; } default_channels[] = {
- { "gsm", CHANNEL_DUPLEX }, /* GSM AT command channel, used by commands/rild/rild.c */
- { "gps", CHANNEL_BROADCAST }, /* GPS NMEA commands, used by libs/hardware_legacy/qemu_gps.c */
- { "control", CHANNEL_DUPLEX }, /* Used for power/leds/vibrator/etc... */
- { NULL, 0 }
-};
-
int main( void )
{
Multiplexer* m = _multiplexer;
@@ -1303,31 +1693,6 @@ int main( void )
multiplexer_init( m, buff );
}
- D("multiplexer inited, creating default channels");
-
- /* now setup all default channels */
- {
- int nn;
-
- for (nn = 0; default_channels[nn].name != NULL; nn++) {
- char control_name[32];
- int fd;
- Channel* chan;
- const char* name = default_channels[nn].name;
- ChannelType ctype = default_channels[nn].ctype;
-
- snprintf(control_name, sizeof(control_name), "%s%s",
- QEMUD_PREFIX, name);
-
- if ((fd = android_get_control_socket(control_name)) < 0) {
- D("couldn't get fd for control socket '%s'", name);
- continue;
- }
- D( "got control socket '%s' on fd %d", control_name, fd);
- multiplexer_add_channel( m, fd, name, ctype );
- }
- }
-
D( "entering main loop");
looper_loop( m->looper );
D( "unexpected termination !!" );
diff --git a/emulator/sensors/Android.mk b/emulator/sensors/Android.mk
new file mode 100644
index 000000000..74e02adb6
--- /dev/null
+++ b/emulator/sensors/Android.mk
@@ -0,0 +1,29 @@
+# Copyright (C) 2009 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+
+
+LOCAL_PATH := $(call my-dir)
+
+ifneq ($(TARGET_PRODUCT),sim)
+# HAL module implemenation, not prelinked and stored in
+# hw/<SENSORS_HARDWARE_MODULE_ID>.<ro.hardware>.so
+include $(CLEAR_VARS)
+LOCAL_PRELINK_MODULE := false
+LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw
+LOCAL_SHARED_LIBRARIES := liblog libcutils
+LOCAL_SRC_FILES := sensors_qemu.c
+LOCAL_MODULE := sensors.goldfish
+LOCAL_MODULE_TAGS := debug
+include $(BUILD_SHARED_LIBRARY)
+endif
diff --git a/emulator/sensors/sensors_qemu.c b/emulator/sensors/sensors_qemu.c
new file mode 100644
index 000000000..85a5af4ed
--- /dev/null
+++ b/emulator/sensors/sensors_qemu.c
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+/* this implements a sensors hardware library for the Android emulator.
+ * the following code should be built as a shared library that will be
+ * placed into /system/lib/hw/sensors.goldfish.so
+ *
+ * it will be loaded by the code in hardware/libhardware/hardware.c
+ * which is itself called from com_android_server_SensorService.cpp
+ */
+
+
+/* we connect with the emulator through the "sensors" qemud service
+ */
+#define SENSORS_SERVICE_NAME "sensors"
+
+#define LOG_TAG "QemuSensors"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+#include <cutils/log.h>
+#include <cutils/sockets.h>
+#include <hardware/sensors.h>
+
+#if 0
+#define D(...) LOGD(__VA_ARGS__)
+#else
+#define D(...) ((void)0)
+#endif
+
+#define E(...) LOGE(__VA_ARGS__)
+
+#include <hardware/qemud.h>
+
+/** SENSOR IDS AND NAMES
+ **/
+
+#define MAX_NUM_SENSORS 4
+
+#define SUPPORTED_SENSORS ((1<<MAX_NUM_SENSORS)-1)
+
+#define ID_BASE SENSORS_HANDLE_BASE
+#define ID_ACCELERATION (ID_BASE+0)
+#define ID_MAGNETIC_FIELD (ID_BASE+1)
+#define ID_ORIENTATION (ID_BASE+2)
+#define ID_TEMPERATURE (ID_BASE+3)
+
+#define SENSORS_ACCELERATION (1 << ID_ACCELERATION)
+#define SENSORS_MAGNETIC_FIELD (1 << ID_MAGNETIC_FIELD)
+#define SENSORS_ORIENTATION (1 << ID_ORIENTATION)
+#define SENSORS_TEMPERATURE (1 << ID_TEMPERATURE)
+
+#define ID_CHECK(x) ((unsigned)((x)-ID_BASE) < 4)
+
+#define SENSORS_LIST \
+ SENSOR_(ACCELERATION,"acceleration") \
+ SENSOR_(MAGNETIC_FIELD,"magnetic-field") \
+ SENSOR_(ORIENTATION,"orientation") \
+ SENSOR_(TEMPERATURE,"temperature") \
+
+static const struct {
+ const char* name;
+ int id; } _sensorIds[MAX_NUM_SENSORS] =
+{
+#define SENSOR_(x,y) { y, ID_##x },
+ SENSORS_LIST
+#undef SENSOR_
+};
+
+static const char*
+_sensorIdToName( int id )
+{
+ int nn;
+ for (nn = 0; nn < MAX_NUM_SENSORS; nn++)
+ if (id == _sensorIds[nn].id)
+ return _sensorIds[nn].name;
+ return "<UNKNOWN>";
+}
+
+static int
+_sensorIdFromName( const char* name )
+{
+ int nn;
+
+ if (name == NULL)
+ return -1;
+
+ for (nn = 0; nn < MAX_NUM_SENSORS; nn++)
+ if (!strcmp(name, _sensorIds[nn].name))
+ return _sensorIds[nn].id;
+
+ return -1;
+}
+
+/** SENSORS CONTROL DEVICE
+ **
+ ** This one is used to send commands to the sensors drivers.
+ ** We implement this by sending directly commands to the emulator
+ ** through the QEMUD channel.
+ **/
+
+typedef struct SensorControl {
+ struct sensors_control_device_t device;
+ int fd;
+ uint32_t active_sensors;
+} SensorControl;
+
+/* this must return a file descriptor that will be used to read
+ * the sensors data (it is passed to data__data_open() below
+ */
+static int
+control__open_data_source(struct sensors_control_device_t *dev)
+{
+ SensorControl* ctl = (void*)dev;
+
+ if (ctl->fd < 0) {
+ ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+ }
+ D("%s: fd=%d", __FUNCTION__, ctl->fd);
+ return ctl->fd;
+}
+
+static int
+control__activate(struct sensors_control_device_t *dev,
+ int handle,
+ int enabled)
+{
+ SensorControl* ctl = (void*)dev;
+ uint32_t mask, sensors, active, new_sensors, changed;
+ char command[128];
+ int ret;
+
+ D("%s: handle=%s (%d) enabled=%d", __FUNCTION__,
+ _sensorIdToName(handle), handle, enabled);
+
+ if (!ID_CHECK(handle)) {
+ E("%s: bad handle ID", __FUNCTION__);
+ return -1;
+ }
+
+ mask = (1<<handle);
+ sensors = enabled ? mask : 0;
+
+ active = ctl->active_sensors;
+ new_sensors = (active & ~mask) | (sensors & mask);
+ changed = active ^ new_sensors;
+
+ if (!changed)
+ return 0;
+
+ snprintf(command, sizeof command, "set:%s:%d",
+ _sensorIdToName(handle), enabled != 0);
+
+ if (ctl->fd < 0) {
+ ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+ }
+
+ ret = qemud_channel_send(ctl->fd, command, -1);
+ if (ret < 0)
+ return -1;
+
+ ctl->active_sensors = new_sensors;
+
+ return 0;
+}
+
+static int
+control__set_delay(struct sensors_control_device_t *dev, int32_t ms)
+{
+ SensorControl* ctl = (void*)dev;
+ char command[128];
+
+ D("%s: dev=%p delay-ms=%d", __FUNCTION__, dev, ms);
+
+ snprintf(command, sizeof command, "set-delay:%d", ms);
+
+ return qemud_channel_send(ctl->fd, command, -1);
+}
+
+/* this function is used to force-stop the blocking read() in
+ * data__poll. In order to keep the implementation as simple
+ * as possible here, we send a command to the emulator which
+ * shall send back an appropriate data block to the system.
+ */
+static int
+control__wake(struct sensors_control_device_t *dev)
+{
+ SensorControl* ctl = (void*)dev;
+ D("%s: dev=%p", __FUNCTION__, dev);
+ return qemud_channel_send(ctl->fd, "wake", -1);
+}
+
+
+static int
+control__close(struct hw_device_t *dev)
+{
+ SensorControl* ctl = (void*)dev;
+ close(ctl->fd);
+ free(ctl);
+ return 0;
+}
+
+/** SENSORS DATA DEVICE
+ **
+ ** This one is used to read sensor data from the hardware.
+ ** We implement this by simply reading the data from the
+ ** emulator through the QEMUD channel.
+ **/
+
+
+typedef struct SensorData {
+ struct sensors_data_device_t device;
+ sensors_data_t sensors[MAX_NUM_SENSORS];
+ int events_fd;
+ uint32_t pendingSensors;
+ int64_t timeStart;
+ int64_t timeOffset;
+} SensorData;
+
+/* return the current time in nanoseconds */
+static int64_t
+data__now_ns(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+
+ return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
+}
+
+static int
+data__data_open(struct sensors_data_device_t *dev, int fd)
+{
+ SensorData* data = (void*)dev;
+ int i;
+ D("%s: dev=%p fd=%d", __FUNCTION__, dev, fd);
+ memset(&data->sensors, 0, sizeof(data->sensors));
+
+ for (i=0 ; i<MAX_NUM_SENSORS ; i++) {
+ data->sensors[i].vector.status = SENSOR_STATUS_ACCURACY_HIGH;
+ }
+ data->pendingSensors = 0;
+ data->timeStart = 0;
+ data->timeOffset = 0;
+
+ data->events_fd = dup(fd);
+ return 0;
+}
+
+static int
+data__data_close(struct sensors_data_device_t *dev)
+{
+ SensorData* data = (void*)dev;
+ D("%s: dev=%p", __FUNCTION__, dev);
+ if (data->events_fd > 0) {
+ close(data->events_fd);
+ data->events_fd = -1;
+ }
+ return 0;
+}
+
+static int
+pick_sensor(SensorData* data,
+ sensors_data_t* values)
+{
+ uint32_t mask = SUPPORTED_SENSORS;
+ while (mask) {
+ uint32_t i = 31 - __builtin_clz(mask);
+ mask &= ~(1<<i);
+ if (data->pendingSensors & (1<<i)) {
+ data->pendingSensors &= ~(1<<i);
+ *values = data->sensors[i];
+ values->sensor = (1<<i);
+ LOGD_IF(0, "%s: %d [%f, %f, %f]", __FUNCTION__,
+ (1<<i),
+ values->vector.x,
+ values->vector.y,
+ values->vector.z);
+ return i;
+ }
+ }
+ LOGE("No sensor to return!!! pendingSensors=%08x", data->pendingSensors);
+ // we may end-up in a busy loop, slow things down, just in case.
+ usleep(100000);
+ return -1;
+}
+
+static int
+data__poll(struct sensors_data_device_t *dev, sensors_data_t* values)
+{
+ SensorData* data = (void*)dev;
+ int fd = data->events_fd;
+
+ D("%s: data=%p", __FUNCTION__, dev);
+
+ // there are pending sensors, returns them now...
+ if (data->pendingSensors) {
+ return pick_sensor(data, values);
+ }
+
+ // wait until we get a complete event for an enabled sensor
+ uint32_t new_sensors = 0;
+
+ while (1) {
+ /* read the next event */
+ char buff[256];
+ int len = qemud_channel_recv(data->events_fd, buff, sizeof buff-1);
+ float params[3];
+ int64_t event_time;
+
+ if (len < 0)
+ continue;
+
+ buff[len] = 0;
+
+ /* "wake" is sent from the emulator to exit this loop. This shall
+ * really be because another thread called "control__wake" in this
+ * process.
+ */
+ if (!strcmp((const char*)data, "wake")) {
+ return 0x7FFFFFFF;
+ }
+
+ /* "acceleration:<x>:<y>:<z>" corresponds to an acceleration event */
+ if (sscanf(buff, "acceleration:%g:%g:%g", params+0, params+1, params+2) == 3) {
+ new_sensors |= SENSORS_ACCELERATION;
+ data->sensors[ID_ACCELERATION].acceleration.x = params[0];
+ data->sensors[ID_ACCELERATION].acceleration.y = params[1];
+ data->sensors[ID_ACCELERATION].acceleration.z = params[2];
+ continue;
+ }
+
+ /* "orientation:<azimuth>:<pitch>:<roll>" is sent when orientation changes */
+ if (sscanf(buff, "orientation:%g:%g:%g", params+0, params+1, params+2) == 3) {
+ new_sensors |= SENSORS_ORIENTATION;
+ data->sensors[ID_ORIENTATION].orientation.azimuth = params[0];
+ data->sensors[ID_ORIENTATION].orientation.pitch = params[1];
+ data->sensors[ID_ORIENTATION].orientation.roll = params[2];
+ continue;
+ }
+
+ /* "magnetic:<x>:<y>:<z>" is sent for the params of the magnetic field */
+ if (sscanf(buff, "magnetic:%g:%g:%g", params+0, params+1, params+2) == 3) {
+ new_sensors |= SENSORS_MAGNETIC_FIELD;
+ data->sensors[ID_MAGNETIC_FIELD].magnetic.x = params[0];
+ data->sensors[ID_MAGNETIC_FIELD].magnetic.y = params[1];
+ data->sensors[ID_MAGNETIC_FIELD].magnetic.z = params[2];
+ continue;
+ }
+
+ /* "temperature:<celsius>" */
+ if (sscanf(buff, "temperature:%g", params+0) == 2) {
+ new_sensors |= SENSORS_TEMPERATURE;
+ data->sensors[ID_TEMPERATURE].temperature = params[0];
+ continue;
+ }
+
+ /* "sync:<time>" is sent after a series of sensor events.
+ * where 'time' is expressed in micro-seconds and corresponds
+ * to the VM time when the real poll occured.
+ */
+ if (sscanf(buff, "sync:%lld", &event_time) == 1) {
+ if (new_sensors) {
+ data->pendingSensors = new_sensors;
+ int64_t t = event_time * 1000LL; /* convert to nano-seconds */
+
+ /* use the time at the first sync: as the base for later
+ * time values */
+ if (data->timeStart == 0) {
+ data->timeStart = data__now_ns();
+ data->timeOffset = data->timeStart - t;
+ }
+ t += data->timeOffset;
+
+ while (new_sensors) {
+ uint32_t i = 31 - __builtin_clz(new_sensors);
+ new_sensors &= ~(1<<i);
+ data->sensors[i].time = t;
+ }
+ return pick_sensor(data, values);
+ } else {
+ D("huh ? sync without any sensor data ?");
+ }
+ continue;
+ }
+ D("huh ? unsupported command");
+ }
+}
+
+static int
+data__close(struct hw_device_t *dev)
+{
+ SensorData* data = (SensorData*)dev;
+ if (data) {
+ if (data->events_fd > 0) {
+ //LOGD("(device close) about to close fd=%d", data->events_fd);
+ close(data->events_fd);
+ }
+ free(data);
+ }
+ return 0;
+}
+
+
+/** MODULE REGISTRATION SUPPORT
+ **
+ ** This is required so that hardware/libhardware/hardware.c
+ ** will dlopen() this library appropriately.
+ **/
+
+/*
+ * the following is the list of all supported sensors.
+ * this table is used to build sSensorList declared below
+ * according to which hardware sensors are reported as
+ * available from the emulator (see get_sensors_list below)
+ *
+ * note: numerical values for maxRange/resolution/power were
+ * taken from the reference AK8976A implementation
+ */
+static const struct sensor_t sSensorListInit[] = {
+ { .name = "Goldfish 3-axis Accelerometer",
+ .vendor = "The Android Open Source Project",
+ .version = 1,
+ .handle = ID_ACCELERATION,
+ .type = SENSOR_TYPE_ACCELEROMETER,
+ .maxRange = 2.8f,
+ .resolution = 1.0f/4032.0f,
+ .power = 3.0f,
+ .reserved = {}
+ },
+
+ { .name = "Goldfish 3-axis Magnetic field sensor",
+ .vendor = "The Android Open Source Project",
+ .version = 1,
+ .handle = ID_MAGNETIC_FIELD,
+ .type = SENSOR_TYPE_MAGNETIC_FIELD,
+ .maxRange = 2000.0f,
+ .resolution = 1.0f,
+ .power = 6.7f,
+ .reserved = {}
+ },
+
+ { .name = "Goldfish Orientation sensor",
+ .vendor = "The Android Open Source Project",
+ .version = 1,
+ .handle = ID_ORIENTATION,
+ .type = SENSOR_TYPE_ORIENTATION,
+ .maxRange = 360.0f,
+ .resolution = 1.0f,
+ .power = 9.7f,
+ .reserved = {}
+ },
+
+ { .name = "Goldfish Temperature sensor",
+ .vendor = "The Android Open Source Project",
+ .version = 1,
+ .handle = ID_TEMPERATURE,
+ .type = SENSOR_TYPE_TEMPERATURE,
+ .maxRange = 80.0f,
+ .resolution = 1.0f,
+ .power = 0.0f,
+ .reserved = {}
+ },
+};
+
+static struct sensor_t sSensorList[MAX_NUM_SENSORS];
+
+static uint32_t sensors__get_sensors_list(struct sensors_module_t* module,
+ struct sensor_t const** list)
+{
+ int fd = qemud_channel_open(SENSORS_SERVICE_NAME);
+ char buffer[12];
+ int mask, nn, count;
+
+ int ret;
+ if (fd < 0) {
+ E("%s: no qemud connection", __FUNCTION__);
+ return 0;
+ }
+ ret = qemud_channel_send(fd, "list-sensors", -1);
+ if (ret < 0) {
+ E("%s: could not query sensor list: %s", __FUNCTION__,
+ strerror(errno));
+ close(fd);
+ return 0;
+ }
+ ret = qemud_channel_recv(fd, buffer, sizeof buffer-1);
+ if (ret < 0) {
+ E("%s: could not receive sensor list: %s", __FUNCTION__,
+ strerror(errno));
+ close(fd);
+ return 0;
+ }
+ buffer[ret] = 0;
+ close(fd);
+
+ /* the result is a integer used as a mask for available sensors */
+ mask = atoi(buffer);
+ count = 0;
+ for (nn = 0; nn < MAX_NUM_SENSORS; nn++) {
+ if (((1 << nn) & mask) == 0)
+ continue;
+
+ sSensorList[count++] = sSensorListInit[nn];
+ }
+ D("%s: returned %d sensors (mask=%d)", __FUNCTION__, count, mask);
+ *list = sSensorList;
+ return count;
+}
+
+
+static int
+open_sensors(const struct hw_module_t* module,
+ const char* name,
+ struct hw_device_t* *device)
+{
+ int status = -EINVAL;
+
+ D("%s: name=%s", __FUNCTION__, name);
+
+ if (!strcmp(name, SENSORS_HARDWARE_CONTROL))
+ {
+ SensorControl *dev = malloc(sizeof(*dev));
+
+ memset(dev, 0, sizeof(*dev));
+
+ dev->device.common.tag = HARDWARE_DEVICE_TAG;
+ dev->device.common.version = 0;
+ dev->device.common.module = (struct hw_module_t*) module;
+ dev->device.common.close = control__close;
+ dev->device.open_data_source = control__open_data_source;
+ dev->device.activate = control__activate;
+ dev->device.set_delay = control__set_delay;
+ dev->device.wake = control__wake;
+ dev->fd = -1;
+
+ *device = &dev->device.common;
+ status = 0;
+ }
+ else if (!strcmp(name, SENSORS_HARDWARE_DATA)) {
+ SensorData *dev = malloc(sizeof(*dev));
+
+ memset(dev, 0, sizeof(*dev));
+
+ dev->device.common.tag = HARDWARE_DEVICE_TAG;
+ dev->device.common.version = 0;
+ dev->device.common.module = (struct hw_module_t*) module;
+ dev->device.common.close = data__close;
+ dev->device.data_open = data__data_open;
+ dev->device.data_close = data__data_close;
+ dev->device.poll = data__poll;
+ dev->events_fd = -1;
+
+ *device = &dev->device.common;
+ status = 0;
+ }
+ return status;
+}
+
+
+static struct hw_module_methods_t sensors_module_methods = {
+ .open = open_sensors
+};
+
+const struct sensors_module_t HAL_MODULE_INFO_SYM = {
+ .common = {
+ .tag = HARDWARE_MODULE_TAG,
+ .version_major = 1,
+ .version_minor = 0,
+ .id = SENSORS_HARDWARE_MODULE_ID,
+ .name = "Goldfish SENSORS Module",
+ .author = "The Android Open Source Project",
+ .methods = &sensors_module_methods,
+ },
+ .get_sensors_list = sensors__get_sensors_list
+};
diff --git a/pdk/README b/pdk/README
index 03af4e895..86621552e 100644
--- a/pdk/README
+++ b/pdk/README
@@ -1,6 +1,9 @@
Building the pdk (platform development kit)
-1) get a cupcake source tree
+1) get a cupcake source tree with all the normal tools... and add doxygen
+(We currently support version 1.4.6)
+
+ sudo apt-get install doxygen
2) from the root
. build/envsetup.sh
diff --git a/samples/ApiDemos/AndroidManifest.xml b/samples/ApiDemos/AndroidManifest.xml
index 0cbba1449..7079e9ecf 100644
--- a/samples/ApiDemos/AndroidManifest.xml
+++ b/samples/ApiDemos/AndroidManifest.xml
@@ -33,8 +33,6 @@
android:label="@string/activity_sample_code"
android:icon="@drawable/app_sample_code" >
- <uses-library android:name="com.google.android.maps" />
-
<activity android:name="ApiDemos">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -1305,20 +1303,6 @@
</intent-filter>
</activity>
- <activity android:name=".view.MapViewDemo" android:label="Views/MapView">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
- <activity android:name=".view.MapViewCompassDemo" android:label="Views/MapView and Compass">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
<!-- ************************************* -->
<!-- GRAPHICS SAMPLES -->
<!-- ************************************* -->
diff --git a/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java b/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java
index a784e1587..1a4b5e4a0 100644
--- a/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java
+++ b/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java
@@ -16,17 +16,18 @@
package com.example.android.apis.app;
+import com.example.android.apis.R;
+
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
+import android.speech.RecognizerIntent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
-import com.example.android.apis.R;
-
import java.util.ArrayList;
/**
@@ -71,9 +72,10 @@ public class VoiceRecognition extends Activity implements OnClickListener {
*/
private void startVoiceRecognitionActivity() {
//TODO Get these values from constants
- Intent intent = new Intent("android.speech.action.RECOGNIZE_SPEECH");
- intent.putExtra("language_model", "free_form");
- intent.putExtra("prompt", "Speech recognition demo");
+ Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speech recognition demo");
startActivityForResult(intent, VOICE_RECOGNITION_REQUEST_CODE);
}
@@ -83,8 +85,8 @@ public class VoiceRecognition extends Activity implements OnClickListener {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {
- //TODO get the value from a constant
- ArrayList<String>matches = data.getStringArrayListExtra("results");
+ ArrayList<String> matches = data.getStringArrayListExtra(
+ RecognizerIntent.EXTRA_RESULTS);
mList.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
matches));
}
diff --git a/samples/ApiDemos/src/com/example/android/apis/graphics/TranslucentGLSurfaceViewActivity.java b/samples/ApiDemos/src/com/example/android/apis/graphics/TranslucentGLSurfaceViewActivity.java
index 750a47ba7..a0bad4bc9 100644
--- a/samples/ApiDemos/src/com/example/android/apis/graphics/TranslucentGLSurfaceViewActivity.java
+++ b/samples/ApiDemos/src/com/example/android/apis/graphics/TranslucentGLSurfaceViewActivity.java
@@ -34,6 +34,10 @@ public class TranslucentGLSurfaceViewActivity extends Activity {
// Create our Preview view and set it as the content of our
// Activity
mGLSurfaceView = new GLSurfaceView(this);
+ // We want an 8888 pixel format because that's required for
+ // a translucent window.
+ // And we want a depth buffer.
+ mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
// Tell the cube renderer that we want to render a translucent version
// of the cube:
mGLSurfaceView.setRenderer(new CubeRenderer(true));
diff --git a/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleActivity.java b/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleActivity.java
index 59f3c6cf8..e5b06f426 100644
--- a/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleActivity.java
+++ b/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleActivity.java
@@ -26,6 +26,7 @@ public class TriangleActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGLView = new GLSurfaceView(this);
+ mGLView.setEGLConfigChooser(false);
mGLView.setRenderer(new TriangleRenderer(this));
setContentView(mGLView);
}
diff --git a/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleRenderer.java b/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleRenderer.java
index 451b927a9..e5299b332 100644
--- a/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleRenderer.java
+++ b/samples/ApiDemos/src/com/example/android/apis/graphics/TriangleRenderer.java
@@ -44,16 +44,6 @@ public class TriangleRenderer implements GLSurfaceView.Renderer{
mTriangle = new Triangle();
}
- public int[] getConfigSpec() {
- // We don't need a depth buffer, and don't care about our
- // color depth.
- int[] configSpec = {
- EGL10.EGL_DEPTH_SIZE, 0,
- EGL10.EGL_NONE
- };
- return configSpec;
- }
-
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
/*
* By default, OpenGL enables features that improve quality
diff --git a/samples/ApiDemos/src/com/example/android/apis/graphics/kube/KubeRenderer.java b/samples/ApiDemos/src/com/example/android/apis/graphics/kube/KubeRenderer.java
index 252f566f9..9977041a2 100644
--- a/samples/ApiDemos/src/com/example/android/apis/graphics/kube/KubeRenderer.java
+++ b/samples/ApiDemos/src/com/example/android/apis/graphics/kube/KubeRenderer.java
@@ -74,15 +74,6 @@ class KubeRenderer implements GLSurfaceView.Renderer {
mWorld.draw(gl);
}
- public int[] getConfigSpec() {
- // Need a depth buffer, don't care about color depth.
- int[] configSpec = {
- EGL10.EGL_DEPTH_SIZE, 16,
- EGL10.EGL_NONE
- };
- return configSpec;
- }
-
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
diff --git a/samples/Compass/src/com/example/android/compass/CompassActivity.java b/samples/Compass/src/com/example/android/compass/CompassActivity.java
index c4b956675..74f3bc71d 100644
--- a/samples/Compass/src/com/example/android/compass/CompassActivity.java
+++ b/samples/Compass/src/com/example/android/compass/CompassActivity.java
@@ -89,16 +89,6 @@ public class CompassActivity extends Activity implements Renderer, SensorEventLi
mSensorManager.unregisterListener(this);
}
- public int[] getConfigSpec() {
- // We want a depth buffer, don't care about the
- // details of the color buffer.
- int[] configSpec = {
- EGL10.EGL_DEPTH_SIZE, 16,
- EGL10.EGL_NONE
- };
- return configSpec;
- }
-
public void onDrawFrame(GL10 gl) {
/*
* Usually, the first thing one might want to do is to clear
diff --git a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
index 9aeb2b54e..50b353601 100644
--- a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
+++ b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java
@@ -550,7 +550,7 @@ public class SoftKeyboard extends InputMethodService
boolean typedWordValid) {
if (suggestions != null && suggestions.size() > 0) {
setCandidatesViewShown(true);
- } else if (isFullscreenMode()) {
+ } else if (isExtractViewShown()) {
setCandidatesViewShown(true);
}
if (mCandidateView != null) {
diff --git a/testrunner/Android.mk b/testrunner/Android.mk
new file mode 100644
index 000000000..93c092841
--- /dev/null
+++ b/testrunner/Android.mk
@@ -0,0 +1,19 @@
+#
+# Install a list of test definitions on device
+#
+
+# where to install the sample files on the device
+#
+local_target_dir := $(TARGET_OUT_DATA)/testinfo
+LOCAL_PATH := $(call my-dir)
+
+########################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := tests.xml
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_PATH := $(local_target_dir)
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+
+include $(BUILD_PREBUILT)
diff --git a/testrunner/android_build.py b/testrunner/android_build.py
new file mode 100644
index 000000000..ca43ecee9
--- /dev/null
+++ b/testrunner/android_build.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python2.4
+#
+#
+# Copyright 2008, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+
+"""Contains utility functions for interacting with the Android build system."""
+
+# Python imports
+import os
+
+# local imports
+import errors
+import logger
+
+
+def GetTop():
+ """Returns the full pathname of the "top" of the Android development tree.
+
+ Assumes build environment has been properly configured by envsetup &
+ lunch/choosecombo.
+
+ Returns:
+ the absolute file path of the Android build root.
+
+ Raises:
+ AbortError: if Android build root could not be found.
+ """
+ # TODO: does this need to be reimplemented to be like gettop() in envsetup.sh
+ root_path = os.getenv('ANDROID_BUILD_TOP')
+ if root_path is None:
+ logger.Log('Error: ANDROID_BUILD_TOP not defined. Please run envsetup.sh')
+ raise errors.AbortError
+ return root_path
diff --git a/testrunner/coverage_targets.xml b/testrunner/coverage_targets.xml
index d7700f63a..32a485659 100644
--- a/testrunner/coverage_targets.xml
+++ b/testrunner/coverage_targets.xml
@@ -108,4 +108,8 @@
<coverage_target name="TelephonyProvider"
build_path="packages/providers/telephony" type="APPS" />
+ <!-- input methods -->
+ <coverage_target name="LatinIME" build_path="packages/inputmethods/LatinIME"
+ type="APPS" />
+
</coverage_targets>
diff --git a/testrunner/runtest.py b/testrunner/runtest.py
new file mode 100755
index 000000000..bf5bb2232
--- /dev/null
+++ b/testrunner/runtest.py
@@ -0,0 +1,280 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+
+"""Command line utility for running a pre-defined test.
+
+Based on previous <androidroot>/development/tools/runtest shell script.
+"""
+
+# Python imports
+import glob
+import optparse
+import os
+from sets import Set
+import sys
+
+# local imports
+import adb_interface
+import android_build
+import coverage
+import errors
+import logger
+import run_command
+import test_defs
+
+
+class TestRunner(object):
+ """Command line utility class for running pre-defined Android test(s)."""
+
+ # file path to android core platform tests, relative to android build root
+ # TODO move these test data files to another directory
+ _CORE_TEST_PATH = os.path.join("development", "testrunner", "tests.xml")
+
+ # vendor glob file path patterns to tests, relative to android
+ # build root
+ _VENDOR_TEST_PATH = os.path.join("vendor", "*", "tests", "testinfo",
+ "tests.xml")
+
+ _RUNTEST_USAGE = (
+ "usage: runtest.py [options] short-test-name[s]\n\n"
+ "The runtest script works in two ways. You can query it "
+ "for a list of tests, or you can launch one or more tests.")
+
+ def _ProcessOptions(self):
+ """Processes command-line options."""
+ # TODO error messages on once-only or mutually-exclusive options.
+ user_test_default = os.path.join(os.environ.get("HOME"), ".android",
+ "tests.xml")
+
+ parser = optparse.OptionParser(usage=self._RUNTEST_USAGE)
+
+ parser.add_option("-l", "--list-tests", dest="only_list_tests",
+ default=False, action="store_true",
+ help="To view the list of tests")
+ parser.add_option("-b", "--skip-build", dest="skip_build", default=False,
+ action="store_true", help="Skip build - just launch")
+ parser.add_option("-n", "--skip_execute", dest="preview", default=False,
+ action="store_true",
+ help="Do not execute, just preview commands")
+ parser.add_option("-r", "--raw-mode", dest="raw_mode", default=False,
+ action="store_true",
+ help="Raw mode (for output to other tools)")
+ parser.add_option("-a", "--suite-assign", dest="suite_assign_mode",
+ default=False, action="store_true",
+ help="Suite assignment (for details & usage see "
+ "InstrumentationTestRunner)")
+ parser.add_option("-v", "--verbose", dest="verbose", default=False,
+ action="store_true",
+ help="Increase verbosity of %s" % sys.argv[0])
+ parser.add_option("-w", "--wait-for-debugger", dest="wait_for_debugger",
+ default=False, action="store_true",
+ help="Wait for debugger before launching tests")
+ parser.add_option("-c", "--test-class", dest="test_class",
+ help="Restrict test to a specific class")
+ parser.add_option("-m", "--test-method", dest="test_method",
+ help="Restrict test to a specific method")
+ parser.add_option("-u", "--user-tests-file", dest="user_tests_file",
+ metavar="FILE", default=user_test_default,
+ help="Alternate source of user test definitions")
+ parser.add_option("-o", "--coverage", dest="coverage",
+ default=False, action="store_true",
+ help="Generate code coverage metrics for test(s)")
+ parser.add_option("-t", "--all-tests", dest="all_tests",
+ default=False, action="store_true",
+ help="Run all defined tests")
+ parser.add_option("--continuous", dest="continuous_tests",
+ default=False, action="store_true",
+ help="Run all tests defined as part of the continuous "
+ "test set")
+
+ group = optparse.OptionGroup(
+ parser, "Targets", "Use these options to direct tests to a specific "
+ "Android target")
+ group.add_option("-e", "--emulator", dest="emulator", default=False,
+ action="store_true", help="use emulator")
+ group.add_option("-d", "--device", dest="device", default=False,
+ action="store_true", help="use device")
+ group.add_option("-s", "--serial", dest="serial",
+ help="use specific serial")
+ parser.add_option_group(group)
+
+ self._options, self._test_args = parser.parse_args()
+
+ if (not self._options.only_list_tests and not self._options.all_tests
+ and not self._options.continuous_tests and len(self._test_args) < 1):
+ parser.print_help()
+ logger.SilentLog("at least one test name must be specified")
+ raise errors.AbortError
+
+ self._adb = adb_interface.AdbInterface()
+ if self._options.emulator:
+ self._adb.SetEmulatorTarget()
+ elif self._options.device:
+ self._adb.SetDeviceTarget()
+ elif self._options.serial is not None:
+ self._adb.SetTargetSerial(self._options.serial)
+
+ if self._options.verbose:
+ logger.SetVerbose(True)
+
+ self._root_path = android_build.GetTop()
+
+ self._known_tests = self._ReadTests()
+
+ self._coverage_gen = coverage.CoverageGenerator(
+ android_root_path=self._root_path, adb_interface=self._adb)
+
+ def _ReadTests(self):
+ """Parses the set of test definition data.
+
+ Returns:
+ A TestDefinitions object that contains the set of parsed tests.
+ Raises:
+ AbortError: If a fatal error occurred when parsing the tests.
+ """
+ core_test_path = os.path.join(self._root_path, self._CORE_TEST_PATH)
+ try:
+ known_tests = test_defs.TestDefinitions()
+ known_tests.Parse(core_test_path)
+ # read all <android root>/vendor/*/tests/testinfo/tests.xml paths
+ vendor_tests_pattern = os.path.join(self._root_path,
+ self._VENDOR_TEST_PATH)
+ test_file_paths = glob.glob(vendor_tests_pattern)
+ for test_file_path in test_file_paths:
+ known_tests.Parse(test_file_path)
+ if os.path.isfile(self._options.user_tests_file):
+ known_tests.Parse(self._options.user_tests_file)
+ return known_tests
+ except errors.ParseError:
+ raise errors.AbortError
+
+ def _DumpTests(self):
+ """Prints out set of defined tests."""
+ print "The following tests are currently defined:"
+ for test in self._known_tests:
+ print test.GetName()
+
+ def _DoBuild(self):
+ logger.SilentLog("Building tests...")
+ target_set = Set()
+ for test_suite in self._GetTestsToRun():
+ self._AddBuildTarget(test_suite.GetBuildPath(), target_set)
+
+ if target_set:
+ if self._options.coverage:
+ self._coverage_gen.EnableCoverageBuild()
+ self._AddBuildTarget(self._coverage_gen.GetEmmaBuildPath(), target_set)
+ target_build_string = " ".join(list(target_set))
+ logger.Log("Building %s" % target_build_string)
+ cmd = 'ONE_SHOT_MAKEFILE="%s" make -C "%s" files' % (target_build_string,
+ self._root_path)
+ if not self._options.preview:
+ run_command.RunCommand(cmd, return_output=False)
+ logger.Log("Syncing to device...")
+ self._adb.Sync()
+
+ def _AddBuildTarget(self, build_dir, target_set):
+ if build_dir is not None:
+ build_file_path = os.path.join(build_dir, "Android.mk")
+ if os.path.isfile(os.path.join(self._root_path, build_file_path)):
+ target_set.add(build_file_path)
+
+ def _GetTestsToRun(self):
+ """Get a list of TestSuite objects to run, based on command line args."""
+ if self._options.all_tests:
+ return self._known_tests.GetTests()
+ if self._options.continuous_tests:
+ return self._known_tests.GetContinuousTests()
+ tests = []
+ for name in self._test_args:
+ test = self._known_tests.GetTest(name)
+ if test is None:
+ logger.Log("Error: Could not find test %s" % name)
+ self._DumpTests()
+ raise errors.AbortError
+ tests.append(test)
+ return tests
+
+ def _RunTest(self, test_suite):
+ """Run the provided test suite.
+
+ Builds up an adb instrument command using provided input arguments.
+
+ Args:
+ test_suite: TestSuite to run
+ """
+
+ test_class = test_suite.GetClassName()
+ if self._options.test_class is not None:
+ test_class = self._options.test_class
+ if self._options.test_method is not None:
+ test_class = "%s#%s" % (test_class, self._options.test_method)
+
+ instrumentation_args = {}
+ if test_class is not None:
+ instrumentation_args["class"] = test_class
+ if self._options.wait_for_debugger:
+ instrumentation_args["debug"] = "true"
+ if self._options.suite_assign_mode:
+ instrumentation_args["suiteAssignment"] = "true"
+ if self._options.coverage:
+ instrumentation_args["coverage"] = "true"
+ if self._options.preview:
+ adb_cmd = self._adb.PreviewInstrumentationCommand(
+ package_name=test_suite.GetPackageName(),
+ runner_name=test_suite.GetRunnerName(),
+ raw_mode=self._options.raw_mode,
+ instrumentation_args=instrumentation_args)
+ logger.Log(adb_cmd)
+ else:
+ self._adb.StartInstrumentationNoResults(
+ package_name=test_suite.GetPackageName(),
+ runner_name=test_suite.GetRunnerName(),
+ raw_mode=self._options.raw_mode,
+ instrumentation_args=instrumentation_args)
+ if self._options.coverage and test_suite.GetTargetName() is not None:
+ coverage_file = self._coverage_gen.ExtractReport(test_suite)
+ if coverage_file is not None:
+ logger.Log("Coverage report generated at %s" % coverage_file)
+
+ def RunTests(self):
+ """Main entry method - executes the tests according to command line args."""
+ try:
+ run_command.SetAbortOnError()
+ self._ProcessOptions()
+ if self._options.only_list_tests:
+ self._DumpTests()
+ return
+
+ if not self._options.skip_build:
+ self._DoBuild()
+
+ for test_suite in self._GetTestsToRun():
+ self._RunTest(test_suite)
+ except KeyboardInterrupt:
+ logger.Log("Exiting...")
+ except errors.AbortError:
+ logger.SilentLog("Exiting due to AbortError...")
+ except errors.WaitForResponseTimedOutError:
+ logger.Log("Timed out waiting for response")
+
+
+def RunTests():
+ runner = TestRunner()
+ runner.RunTests()
+
+if __name__ == "__main__":
+ RunTests()
diff --git a/testrunner/tests.xml b/testrunner/tests.xml
index 8d9c0ab29..d186af4c7 100644
--- a/testrunner/tests.xml
+++ b/testrunner/tests.xml
@@ -87,6 +87,12 @@ These attributes map to the following commands:
coverage_target="ApiDemos"
continuous="true" />
+<test name="launchperf"
+ build_path="development/apps/launchperf"
+ package="com.android.launchperf"
+ class="com.android.launchperf.SimpleActivityLaunchPerformance"
+ coverage_target="framework" />
+
<!-- targeted framework tests -->
<test name="heap"
build_path="frameworks/base/tests/AndroidTests"
@@ -114,6 +120,11 @@ These attributes map to the following commands:
class="android.content.AbstractTableMergerTest"
coverage_target="framework" />
+<test name="imf"
+ build_path="frameworks/base/tests/ImfTest"
+ package="com.android.imftest.tests"
+ coverage_target="framework"
+ continuous="true" />
<!-- selected app tests -->
<test name="browser"
@@ -169,12 +180,19 @@ These attributes map to the following commands:
runner=".MediaFrameworkTestRunner"
coverage_target="framework"
continuous="true" />
-
+
<test name="mediaunit"
build_path="frameworks/base/media/tests/MediaFrameworkTest"
package="com.android.mediaframeworktest"
runner=".MediaFrameworkUnitTestRunner"
coverage_target="framework" />
+
+<test name="musicplayer"
+ build_path="packages/apps/Music"
+ package="com.android.music.tests"
+ runner=".MusicPlayerFunctionalTestRunner"
+ coverage_target="Music"
+ continuous="true" />
<!-- obsolete?
<test name="mediaprov"
diff --git a/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java b/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java
index d2c71624d..6444e4d01 100644
--- a/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java
+++ b/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java
@@ -181,11 +181,14 @@ public final class AaptExecLoopTask extends Task {
task.createArg().setValue("-M");
task.createArg().setValue(mManifest);
- // resources location
- task.createArg().setValue("-S");
- task.createArg().setValue(mResources);
+ // resources location. This may not exists, and aapt doesn't like it, so we check first.
+ File res = new File(mResources);
+ if (res.isDirectory()) {
+ task.createArg().setValue("-S");
+ task.createArg().setValue(mResources);
+ }
- // assets location. this may not exists, and aapt doesn't like it, so we check first.
+ // assets location. This may not exists, and aapt doesn't like it, so we check first.
File assets = new File(mAssets);
if (assets.isDirectory()) {
task.createArg().setValue("-A");
diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
index bc1834f61..1a0b21fdb 100755
--- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
+++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java
@@ -27,7 +27,14 @@ import com.android.ddmlib.MultiLineReceiver;
* <p>Expects the following output:
*
* <p>If fatal error occurred when attempted to run the tests:
- * <pre> INSTRUMENTATION_FAILED: </pre>
+ * <pre>
+ * INSTRUMENTATION_STATUS: Error=error Message
+ * INSTRUMENTATION_FAILED:
+ * </pre>
+ * <p>or
+ * <pre>
+ * INSTRUMENTATION_RESULT: shortMsg=error Message
+ * </pre>
*
* <p>Otherwise, expect a series of test results, each one containing a set of status key/value
* pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
@@ -56,6 +63,8 @@ public class InstrumentationResultParser extends MultiLineReceiver {
private static final String CLASS = "class";
private static final String STACK = "stack";
private static final String NUMTESTS = "numtests";
+ private static final String ERROR = "Error";
+ private static final String SHORTMSG = "shortMsg";
}
/** Test result status codes. */
@@ -71,6 +80,8 @@ public class InstrumentationResultParser extends MultiLineReceiver {
private static final String STATUS = "INSTRUMENTATION_STATUS: ";
private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
+ private static final String CODE = "INSTRUMENTATION_CODE: ";
+ private static final String RESULT = "INSTRUMENTATION_RESULT: ";
private static final String TIME_REPORT = "Time: ";
}
@@ -90,6 +101,23 @@ public class InstrumentationResultParser extends MultiLineReceiver {
boolean isComplete() {
return mCode != null && mTestName != null && mTestClass != null;
}
+
+ /** Provides a more user readable string for TestResult, if possible */
+ @Override
+ public String toString() {
+ StringBuilder output = new StringBuilder();
+ if (mTestClass != null ) {
+ output.append(mTestClass);
+ output.append('#');
+ }
+ if (mTestName != null) {
+ output.append(mTestName);
+ }
+ if (output.length() > 0) {
+ return output.toString();
+ }
+ return "unknown result";
+ }
}
/** Stores the status values for the test result currently being parsed */
@@ -130,6 +158,8 @@ public class InstrumentationResultParser extends MultiLineReceiver {
public void processNewLines(String[] lines) {
for (String line : lines) {
parse(line);
+ // in verbose mode, dump all adb output to log
+ Log.v(LOG_TAG, line);
}
}
@@ -160,9 +190,15 @@ public class InstrumentationResultParser extends MultiLineReceiver {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
parseKey(line, Prefixes.STATUS.length());
- } else if (line.startsWith(Prefixes.STATUS_FAILED)) {
- Log.e(LOG_TAG, "test run failed " + line);
- mTestListener.testRunFailed(line);
+ } else if (line.startsWith(Prefixes.RESULT)) {
+ // Previous status key-value has been collected. Store it.
+ submitCurrentKeyValue();
+ parseKey(line, Prefixes.RESULT.length());
+ } else if (line.startsWith(Prefixes.STATUS_FAILED) ||
+ line.startsWith(Prefixes.CODE)) {
+ // Previous status key-value has been collected. Store it.
+ submitCurrentKeyValue();
+ // just ignore the remaining data on this line
} else if (line.startsWith(Prefixes.TIME_REPORT)) {
parseTime(line, Prefixes.TIME_REPORT.length());
} else {
@@ -186,19 +222,19 @@ public class InstrumentationResultParser extends MultiLineReceiver {
if (mCurrentKey.equals(StatusKeys.CLASS)) {
testInfo.mTestClass = statusValue.trim();
- }
- else if (mCurrentKey.equals(StatusKeys.TEST)) {
+ } else if (mCurrentKey.equals(StatusKeys.TEST)) {
testInfo.mTestName = statusValue.trim();
- }
- else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
+ } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
try {
testInfo.mNumTests = Integer.parseInt(statusValue);
- }
- catch (NumberFormatException e) {
+ } catch (NumberFormatException e) {
Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue);
}
- }
- else if (mCurrentKey.equals(StatusKeys.STACK)) {
+ } else if (mCurrentKey.equals(StatusKeys.ERROR) ||
+ mCurrentKey.equals(StatusKeys.SHORTMSG)) {
+ // test run must have failed
+ handleTestRunFailed(statusValue);
+ } else if (mCurrentKey.equals(StatusKeys.STACK)) {
testInfo.mStackTrace = statusValue;
}
@@ -229,7 +265,7 @@ public class InstrumentationResultParser extends MultiLineReceiver {
int endKeyPos = line.indexOf('=', keyStartPos);
if (endKeyPos != -1) {
mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
- parseValue(line, endKeyPos+1);
+ parseValue(line, endKeyPos + 1);
}
}
@@ -252,8 +288,7 @@ public class InstrumentationResultParser extends MultiLineReceiver {
TestResult testInfo = getCurrentTestInfo();
try {
testInfo.mCode = Integer.parseInt(value);
- }
- catch (NumberFormatException e) {
+ } catch (NumberFormatException e) {
Log.e(LOG_TAG, "Expected integer status code, received: " + value);
}
@@ -286,7 +321,7 @@ public class InstrumentationResultParser extends MultiLineReceiver {
*/
private void reportResult(TestResult testInfo) {
if (!testInfo.isComplete()) {
- Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
+ Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
return;
}
reportTestRunStarted(testInfo);
@@ -337,8 +372,7 @@ public class InstrumentationResultParser extends MultiLineReceiver {
private String getTrace(TestResult testInfo) {
if (testInfo.mStackTrace != null) {
return testInfo.mStackTrace;
- }
- else {
+ } else {
Log.e(LOG_TAG, "Could not find stack trace for failed test ");
return new Throwable("Unknown failure").toString();
}
@@ -351,14 +385,20 @@ public class InstrumentationResultParser extends MultiLineReceiver {
String timeString = line.substring(startPos);
try {
float timeSeconds = Float.parseFloat(timeString);
- mTestTime = (long)(timeSeconds * 1000);
- }
- catch (NumberFormatException e) {
+ mTestTime = (long) (timeSeconds * 1000);
+ } catch (NumberFormatException e) {
Log.e(LOG_TAG, "Unexpected time format " + timeString);
}
}
/**
+ * Process a instrumentation run failure
+ */
+ private void handleTestRunFailed(String errorMsg) {
+ mTestListener.testRunFailed(errorMsg == null ? "Unknown error" : errorMsg);
+ }
+
+ /**
* Called by parent when adb session is complete.
*/
@Override
diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
index 4edbbbbd3..999542634 100644
--- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
+++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
@@ -21,27 +21,35 @@ import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import java.io.IOException;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Map.Entry;
/**
* Runs a Android test command remotely and reports results.
*/
public class RemoteAndroidTestRunner {
- private static final char CLASS_SEPARATOR = ',';
- private static final char METHOD_SEPARATOR = '#';
- private static final char RUNNER_SEPARATOR = '/';
- private String mClassArg;
private final String mPackageName;
private final String mRunnerName;
- private String mExtraArgs;
- private boolean mLogOnlyMode;
private IDevice mRemoteDevice;
+ /** map of name-value instrumentation argument pairs */
+ private Map<String, String> mArgMap;
private InstrumentationResultParser mParser;
private static final String LOG_TAG = "RemoteAndroidTest";
- private static final String DEFAULT_RUNNER_NAME =
- "android.test.InstrumentationTestRunner";
-
+ private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner";
+
+ private static final char CLASS_SEPARATOR = ',';
+ private static final char METHOD_SEPARATOR = '#';
+ private static final char RUNNER_SEPARATOR = '/';
+
+ // defined instrumentation argument names
+ private static final String CLASS_ARG_NAME = "class";
+ private static final String LOG_ARG_NAME = "log";
+ private static final String DEBUG_ARG_NAME = "debug";
+ private static final String COVERAGE_ARG_NAME = "coverage";
+
/**
* Creates a remote Android test runner.
*
@@ -56,12 +64,10 @@ public class RemoteAndroidTestRunner {
mPackageName = packageName;
mRunnerName = runnerName;
- mRemoteDevice = remoteDevice;
- mClassArg = null;
- mExtraArgs = "";
- mLogOnlyMode = false;
+ mRemoteDevice = remoteDevice;
+ mArgMap = new Hashtable<String, String>();
}
-
+
/**
* Alternate constructor. Uses default instrumentation runner.
*
@@ -72,7 +78,7 @@ public class RemoteAndroidTestRunner {
IDevice remoteDevice) {
this(packageName, null, remoteDevice);
}
-
+
/**
* Returns the application package name.
*/
@@ -89,14 +95,14 @@ public class RemoteAndroidTestRunner {
}
return mRunnerName;
}
-
+
/**
* Returns the complete instrumentation component path.
*/
private String getRunnerPath() {
return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
}
-
+
/**
* Sets to run only tests in this class
* Must be called before 'run'.
@@ -104,7 +110,7 @@ public class RemoteAndroidTestRunner {
* @param className fully qualified class name (eg x.y.z)
*/
public void setClassName(String className) {
- mClassArg = className;
+ addInstrumentationArg(CLASS_ARG_NAME, className);
}
/**
@@ -119,15 +125,15 @@ public class RemoteAndroidTestRunner {
public void setClassNames(String[] classNames) {
StringBuilder classArgBuilder = new StringBuilder();
- for (int i=0; i < classNames.length; i++) {
+ for (int i = 0; i < classNames.length; i++) {
if (i != 0) {
classArgBuilder.append(CLASS_SEPARATOR);
}
classArgBuilder.append(classNames[i]);
}
- mClassArg = classArgBuilder.toString();
+ setClassName(classArgBuilder.toString());
}
-
+
/**
* Sets to run only specified test method
* Must be called before 'run'.
@@ -136,47 +142,70 @@ public class RemoteAndroidTestRunner {
* @param testName method name
*/
public void setMethodName(String className, String testName) {
- mClassArg = className + METHOD_SEPARATOR + testName;
+ setClassName(className + METHOD_SEPARATOR + testName);
}
-
+
/**
- * Sets extra arguments to include in instrumentation command.
- * Must be called before 'run'.
+ * Adds a argument to include in instrumentation command.
+ * <p/>
+ * Must be called before 'run'. If an argument with given name has already been provided, it's
+ * value will be overridden.
*
- * @param instrumentationArgs must not be null
+ * @param name the name of the instrumentation bundle argument
+ * @param value the value of the argument
*/
- public void setExtraArgs(String instrumentationArgs) {
- if (instrumentationArgs == null) {
- throw new IllegalArgumentException("instrumentationArgs cannot be null");
+ public void addInstrumentationArg(String name, String value) {
+ if (name == null || value == null) {
+ throw new IllegalArgumentException("name or value arguments cannot be null");
}
- mExtraArgs = instrumentationArgs;
+ mArgMap.put(name, value);
}
-
+
/**
- * Returns the extra instrumentation arguments.
+ * Adds a boolean argument to include in instrumentation command.
+ * <p/>
+ * @see RemoteAndroidTestRunner#addInstrumentationArg
+ *
+ * @param name the name of the instrumentation bundle argument
+ * @param value the value of the argument
*/
- public String getExtraArgs() {
- return mExtraArgs;
+ public void addBooleanArg(String name, boolean value) {
+ addInstrumentationArg(name, Boolean.toString(value));
}
-
+
/**
* Sets this test run to log only mode - skips test execution.
*/
public void setLogOnly(boolean logOnly) {
- mLogOnlyMode = logOnly;
+ addBooleanArg(LOG_ARG_NAME, logOnly);
}
-
+
+ /**
+ * Sets this debug mode of this test run. If true, the Android test runner will wait for a
+ * debugger to attach before proceeding with test execution.
+ */
+ public void setDebug(boolean debug) {
+ addBooleanArg(DEBUG_ARG_NAME, debug);
+ }
+
+ /**
+ * Sets this code coverage mode of this test run.
+ */
+ public void setCoverage(boolean coverage) {
+ addBooleanArg(COVERAGE_ARG_NAME, coverage);
+ }
+
/**
* Execute this test run.
*
* @param listener listens for test results
*/
public void run(ITestRunListener listener) {
- final String runCaseCommandStr = "am instrument -w -r "
- + getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath();
+ final String runCaseCommandStr = String.format("am instrument -w -r %s %s",
+ getArgsCommand(), getRunnerPath());
Log.d(LOG_TAG, runCaseCommandStr);
mParser = new InstrumentationResultParser(listener);
-
+
try {
mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser);
} catch (IOException e) {
@@ -184,7 +213,7 @@ public class RemoteAndroidTestRunner {
listener.testRunFailed(e.toString());
}
}
-
+
/**
* Requests cancellation of this test run.
*/
@@ -193,36 +222,19 @@ public class RemoteAndroidTestRunner {
mParser.cancel();
}
}
-
- /**
- * Returns the test class argument.
- */
- private String getClassArg() {
- return mClassArg;
- }
-
- /**
- * Returns the full instrumentation command which specifies the test classes to execute.
- * Returns an empty string if no classes were specified.
- */
- private String getClassCmd() {
- String classArg = getClassArg();
- if (classArg != null) {
- return "-e class " + classArg;
- }
- return "";
- }
/**
- * Returns the full command to enable log only mode - if specified. Otherwise returns an
- * empty string.
+ * Returns the full instrumentation command line syntax for the provided instrumentation
+ * arguments.
+ * Returns an empty string if no arguments were specified.
*/
- private String getLogCmd() {
- if (mLogOnlyMode) {
- return "-e log true";
- }
- else {
- return "";
+ private String getArgsCommand() {
+ StringBuilder commandBuilder = new StringBuilder();
+ for (Entry<String, String> argPair : mArgMap.entrySet()) {
+ final String argCmd = String.format(" -e %s %s", argPair.getKey(),
+ argPair.getValue());
+ commandBuilder.append(argCmd);
}
+ return commandBuilder.toString();
}
}
diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
index 77d10c1d1..7742dd655 100644
--- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
+++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java
@@ -103,9 +103,43 @@ public class InstrumentationResultParserTest extends TestCase {
injectTestString(timeString);
assertEquals(4900, mTestResult.mTestTime);
}
+
+ /**
+ * Test basic parsing of a test run failure.
+ */
+ public void testRunFailed() {
+ StringBuilder output = new StringBuilder();
+ final String errorMessage = "Unable to find instrumentation info";
+ addStatusKey(output, "Error", errorMessage);
+ addStatusCode(output, "-1");
+ output.append("INSTRUMENTATION_FAILED: com.dummy/android.test.InstrumentationTestRunner");
+ addLineBreak(output);
+
+ injectTestString(output.toString());
+
+ assertEquals(errorMessage, mTestResult.mRunFailedMessage);
+ }
+
+ /**
+ * Test parsing of a test run failure, where an instrumentation component failed to load
+ * Parsing input takes the from of INSTRUMENTATION_RESULT: fff
+ */
+ public void testRunFailedResult() {
+ StringBuilder output = new StringBuilder();
+ final String errorMessage = "Unable to instantiate instrumentation";
+ output.append("INSTRUMENTATION_RESULT: shortMsg=");
+ output.append(errorMessage);
+ addLineBreak(output);
+ output.append("INSTRUMENTATION_CODE: 0");
+ addLineBreak(output);
+
+ injectTestString(output.toString());
+
+ assertEquals(errorMessage, mTestResult.mRunFailedMessage);
+ }
/**
- * builds a common test result using TEST_NAME and TEST_CLASS.
+ * Builds a common test result using TEST_NAME and TEST_CLASS.
*/
private StringBuilder buildCommonResult() {
StringBuilder output = new StringBuilder();
@@ -146,6 +180,13 @@ public class InstrumentationResultParserTest extends TestCase {
outputBuilder.append(key);
outputBuilder.append('=');
outputBuilder.append(value);
+ addLineBreak(outputBuilder);
+ }
+
+ /**
+ * Append line break characters to output
+ */
+ private void addLineBreak(StringBuilder outputBuilder) {
outputBuilder.append("\r\n");
}
@@ -164,7 +205,7 @@ public class InstrumentationResultParserTest extends TestCase {
private void addStatusCode(StringBuilder outputBuilder, String value) {
outputBuilder.append("INSTRUMENTATION_STATUS_CODE: ");
outputBuilder.append(value);
- outputBuilder.append("\r\n");
+ addLineBreak(outputBuilder);
}
/**
@@ -197,11 +238,14 @@ public class InstrumentationResultParserTest extends TestCase {
TestFailure mTestStatus;
String mTrace;
boolean mStopped;
+ /** stores the error message provided to testRunFailed */
+ String mRunFailedMessage;
VerifyingTestResult() {
mNumTestsRun = 0;
mTestStatus = null;
mStopped = false;
+ mRunFailedMessage = null;
}
public void testEnded(TestIdentifier test) {
@@ -238,8 +282,7 @@ public class InstrumentationResultParserTest extends TestCase {
}
public void testRunFailed(String errorMessage) {
- // ignored
+ mRunFailedMessage = errorMessage;
}
}
-
}
diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
index 9acaaf954..6a653ad05 100644
--- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
+++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java
@@ -17,18 +17,17 @@
package com.android.ddmlib.testrunner;
import com.android.ddmlib.Client;
+import com.android.ddmlib.Device.DeviceState;
import com.android.ddmlib.FileListingService;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.log.LogReceiver;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.SyncService;
-import com.android.ddmlib.Device.DeviceState;
-import com.android.ddmlib.log.LogReceiver;
-
-import junit.framework.TestCase;
import java.io.IOException;
import java.util.Map;
+import junit.framework.TestCase;
/**
* Tests RemoteAndroidTestRunner.
@@ -82,14 +81,15 @@ public class RemoteAndroidTestRunnerTest extends TestCase {
}
/**
- * Test the building of the instrumentation runner command with extra args set.
+ * Test the building of the instrumentation runner command with extra argument added.
*/
- public void testRunWithExtraArgs() {
- final String extraArgs = "blah";
- mRunner.setExtraArgs(extraArgs);
+ public void testRunWithAddInstrumentationArg() {
+ final String extraArgName = "blah";
+ final String extraArgValue = "blahValue";
+ mRunner.addInstrumentationArg(extraArgName, extraArgValue);
mRunner.run(new EmptyListener());
- assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs,
- TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
+ assertStringsEquals(String.format("am instrument -w -r -e %s %s %s/%s", extraArgName,
+ extraArgValue, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand());
}
@@ -243,6 +243,5 @@ public class RemoteAndroidTestRunnerTest extends TestCase {
public void testStarted(TestIdentifier test) {
// ignore
}
-
}
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
index 3750f6602..c0dfcefd5 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
@@ -40,7 +40,10 @@ Require-Bundle: com.android.ide.eclipse.ddms,
org.eclipse.wst.sse.ui,
org.eclipse.wst.xml.core,
org.eclipse.wst.xml.ui,
- org.eclipse.jdt.junit
+ org.eclipse.jdt.junit,
+ 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/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png
new file mode 100644
index 000000000..25826dce7
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png
Binary files differ
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index 39e6dd584..2c1394cd2 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -470,7 +470,7 @@
point="org.eclipse.ui.actionSets">
<actionSet
description="Android Wizards"
- id="adt.actionSet1"
+ id="adt.actionSet.wizards"
label="Android Wizards"
visible="true">
<action
@@ -481,12 +481,6 @@
style="push"
toolbarPath="android_project"
tooltip="Opens a wizard to help create a new Android XML file">
- <enablement>
- <objectState
- name="projectNature"
- value="com.android.ide.eclipse.adt.AndroidNature">
- </objectState>
- </enablement>
</action>
<action
class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
@@ -498,6 +492,21 @@
tooltip="Opens a wizard to help create a new Android project">
</action>
</actionSet>
+ <actionSet
+ description="Refactorings for Android"
+ id="adt.actionSet.refactorings"
+ label="Android Refactorings"
+ visible="true">
+ <action
+ class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringAction"
+ definitionId="com.android.ide.eclipse.adt.refactoring.extract.string"
+ id="com.android.ide.eclipse.adt.actions.ExtractString"
+ label="Extract Android String..."
+ menubarPath="org.eclipse.jdt.ui.refactoring.menu/codingGroup"
+ style="push"
+ tooltip="Extracts a string into Android resource string">
+ </action>
+ </actionSet>
</extension>
<extension
point="org.eclipse.debug.core.launchDelegates">
@@ -506,8 +515,84 @@
delegateDescription="Removes the Android JAR from the Bootstrap Classpath"
id="com.android.ide.eclipse.adt.launch.JUnitLaunchConfigDelegate.launchAndroidJunit"
modes="run,debug"
- name="Android JUnit"
+ name="Android JUnit Test"
type="org.eclipse.jdt.junit.launchconfig">
</launchDelegate>
</extension>
+ <extension
+ point="org.eclipse.debug.core.launchConfigurationTypes">
+ <launchConfigurationType
+ delegate="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchConfigDelegate"
+ id="com.android.ide.eclipse.adt.junit.launchConfigurationType"
+ modes="run,debug"
+ name="Android JUnit Test"
+ public="true"
+ sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"
+ sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer">
+ </launchConfigurationType>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchConfigurationTypeImages">
+ <launchConfigurationTypeImage
+ configTypeID="com.android.ide.eclipse.adt.junit.launchConfigurationType"
+ icon="icons/androidjunit.png"
+ id="com.android.ide.eclipse.adt.junit.launchConfigurationTypeImage">
+ </launchConfigurationTypeImage>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchConfigurationTabGroups">
+ <launchConfigurationTabGroup
+ class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitTabGroup"
+ description="Android JUnit Test"
+ id="com.android.ide.eclipse.adt.junit.AndroidJUnitLaunchConfigTabGroup"
+ type="com.android.ide.eclipse.adt.junit.launchConfigurationType"/>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchShortcuts">
+ <shortcut
+ class="com.android.ide.eclipse.adt.launch.junit.AndroidJUnitLaunchShortcut"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.adt.junit.launchShortcut"
+ label="Android JUnit Test"
+ modes="run,debug">
+ <contextualLaunch>
+ <enablement>
+ <with variable="selection">
+ <count value="1"/>
+ <iterate>
+ <adapt type="org.eclipse.jdt.core.IJavaElement">
+ <test property="org.eclipse.jdt.core.isInJavaProjectWithNature" value="com.android.ide.eclipse.adt.AndroidNature"/>
+ <test property="org.eclipse.jdt.core.hasTypeOnClasspath" value="junit.framework.Test"/>
+ <test property="org.eclipse.jdt.junit.canLaunchAsJUnit" forcePluginActivation="true"/>
+ </adapt>
+ </iterate>
+ </with>
+ </enablement>
+ </contextualLaunch>
+ <configurationType
+ id="com.android.ide.eclipse.adt.junit.launchConfigurationType">
+ </configurationType>
+ </shortcut>
+ </extension>
+ <extension
+ point="org.eclipse.ui.commands">
+ <category
+ description="Refactorings for Android Projects"
+ id="com.android.ide.eclipse.adt.refactoring.category"
+ name="Android Refactorings">
+ </category>
+ <command
+ categoryId="com.android.ide.eclipse.adt.refactoring.category"
+ description="Extract Strings into Android String Resources"
+ id="com.android.ide.eclipse.adt.refactoring.extract.string"
+ name="Extract Android String">
+ </command>
+ </extension>
+ <extension
+ point="org.eclipse.ltk.core.refactoring.refactoringContributions">
+ <contribution
+ class="com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringContribution"
+ id="com.android.ide.eclipse.adt.refactoring.extract.string">
+ </contribution>
+ </extension>
</plugin>
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
index 48a21d1d6..42db64a61 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
@@ -1040,6 +1040,16 @@ public class AdtPlugin extends AbstractUIPlugin {
mSdkIsLoaded = LoadStatus.LOADED;
progress.setTaskName("Check Projects");
+
+ ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
+ for (IJavaProject javaProject : mPostLoadProjectsToResolve) {
+ if (javaProject.getProject().isOpen()) {
+ list.add(javaProject);
+ }
+ }
+
+ // done with this list.
+ mPostLoadProjectsToResolve.clear();
// check the projects that need checking.
// The method modifies the list (it removes the project that
@@ -1047,14 +1057,13 @@ public class AdtPlugin extends AbstractUIPlugin {
AndroidClasspathContainerInitializer.checkProjectsCache(
mPostLoadProjectsToCheck);
- mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck);
+ list.addAll(mPostLoadProjectsToCheck);
// update the project that needs recompiling.
- if (mPostLoadProjectsToResolve.size() > 0) {
- IJavaProject[] array = mPostLoadProjectsToResolve.toArray(
- new IJavaProject[mPostLoadProjectsToResolve.size()]);
+ if (list.size() > 0) {
+ IJavaProject[] array = list.toArray(
+ new IJavaProject[list.size()]);
AndroidClasspathContainerInitializer.updateProjects(array);
- mPostLoadProjectsToResolve.clear();
}
progress.worked(10);
@@ -1273,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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java
index f8a969e94..1edcf79fd 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java
+++ b/tools/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 {
@@ -979,7 +979,10 @@ public class ApkBuilder extends BaseBuilder {
writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list);
for (IJavaProject referencedJavaProject : referencedJavaProjects) {
- if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE)) {
+ // only include output from non android referenced project
+ // (This is to handle the case of reference Android projects in the context of
+ // instrumentation projects that need to reference the projects to be tested).
+ if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE) == false) {
writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list);
}
}
@@ -1084,7 +1087,10 @@ public class ApkBuilder extends BaseBuilder {
IWorkspaceRoot wsRoot = ws.getRoot();
for (IJavaProject javaProject : referencedJavaProjects) {
- if (javaProject.getProject().hasNature(AndroidConstants.NATURE)) {
+ // only include output from non android referenced project
+ // (This is to handle the case of reference Android projects in the context of
+ // instrumentation projects that need to reference the projects to be tested).
+ if (javaProject.getProject().hasNature(AndroidConstants.NATURE) == false) {
// get the output folder
IPath path = null;
try {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
index c5082838a..6f9c2f16a 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
+++ b/tools/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 {
@@ -222,6 +222,7 @@ public class PreCompilerBuilder extends BaseBuilder {
PreCompilerDeltaVisitor dv = null;
String javaPackage = null;
+ int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
if (kind == FULL_BUILD) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
@@ -253,6 +254,7 @@ public class PreCompilerBuilder extends BaseBuilder {
// get the java package from the visitor
javaPackage = dv.getManifestPackage();
+ minSdkVersion = dv.getMinSdkVersion();
}
}
@@ -276,7 +278,7 @@ public class PreCompilerBuilder extends BaseBuilder {
if (manifest == null) {
String msg = String.format(Messages.s_File_Missing,
AndroidConstants.FN_ANDROID_MANIFEST);
- AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+ AdtPlugin.printErrorToConsole(project, msg);
markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// This interrupts the build. The next builders will not run.
@@ -304,19 +306,34 @@ public class PreCompilerBuilder extends BaseBuilder {
// get the java package from the parser
javaPackage = parser.getPackage();
+ minSdkVersion = parser.getApiLevelRequirement();
}
+
+ if (minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK &&
+ minSdkVersion < projectTarget.getApiVersionNumber()) {
+ // check it against the target api level
+ String msg = String.format(
+ "Manifest min SDK version (%1$d) is lower than project target API level (%2$d)",
+ minSdkVersion, projectTarget.getApiVersionNumber());
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg,
+ IMarker.SEVERITY_ERROR);
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ }
+
if (javaPackage == null || javaPackage.length() == 0) {
// looks like the AndroidManifest file isn't valid.
String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
AndroidConstants.FN_ANDROID_MANIFEST);
- AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
- msg);
+ AdtPlugin.printErrorToConsole(project, msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
// This interrupts the build. The next builders will not run.
stopBuild(msg);
}
-
+
// at this point we have the java package. We need to make sure it's not a different
// package than the previous one that were built.
if (javaPackage.equals(mManifestPackage) == false) {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
index 6841830e7..29942e8c7 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
@@ -72,8 +72,10 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
/** Manifest check/parsing flag. */
private boolean mCheckedManifestXml = false;
- /** Application Pacakge, gathered from the parsing of the manifest */
+ /** Application Package, gathered from the parsing of the manifest */
private String mJavaPackage = null;
+ /** minSDKVersion attribute value, gathered from the parsing of the manifest */
+ private int mMinSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
// Internal usage fields.
/**
@@ -137,6 +139,22 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
return mJavaPackage;
}
+ /**
+ * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
+ * <p/>
+ * This can return {@link AndroidManifestParser#INVALID_MIN_SDK} in two cases:
+ * <ul>
+ * <li>The manifest was not part of the resource change delta, and the manifest was
+ * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
+ * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
+ * but the package declaration is missing</li>
+ * </ul>
+ * @return the minSdkVersion or {@link AndroidManifestParser#INVALID_MIN_SDK}.
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
/*
* (non-Javadoc)
*
@@ -184,6 +202,7 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
if (parser != null) {
mJavaPackage = parser.getPackage();
+ mMinSdkVersion = parser.getApiLevelRequirement();
}
mCheckedManifestXml = true;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java
index 035aa5b73..b1f9ec1d4 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java
+++ b/tools/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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java
index 448cda6b5..b5ea7275c 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java
@@ -32,16 +32,41 @@ public class AndroidLaunchConfiguration {
*/
public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
- public static final boolean AUTO_TARGET_MODE = true;
-
+ /**
+ * Target selection mode for the configuration: either {@link #AUTO} or {@link #MANUAL}.
+ */
+ public enum TargetMode {
+ /** Automatic target selection mode. */
+ AUTO(true),
+ /** Manual target selection mode. */
+ MANUAL(false);
+
+ private boolean mValue;
+
+ TargetMode(boolean value) {
+ mValue = value;
+ }
+
+ public boolean getValue() {
+ return mValue;
+ }
+
+ public static TargetMode getMode(boolean value) {
+ for (TargetMode mode : values()) {
+ if (mode.mValue == value) {
+ return mode;
+ }
+ }
+
+ return null;
+ }
+ }
+
/**
* Target selection mode.
- * <ul>
- * <li><code>true</code>: automatic mode, see {@link #AUTO_TARGET_MODE}</li>
- * <li><code>false</code>: manual mode</li>
- * </ul>
+ * @see TargetMode
*/
- 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 +106,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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
index 88ee8b6ff..fafc4020b 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java
@@ -17,9 +17,6 @@
package com.android.ide.eclipse.adt.launch;
import com.android.ddmlib.AndroidDebugBridge;
-import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
-import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
-import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Client;
import com.android.ddmlib.ClientData;
import com.android.ddmlib.Device;
@@ -27,13 +24,19 @@ import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.SyncService;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
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.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.avd.AvdManager;
@@ -52,6 +55,8 @@ 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.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 +69,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;
@@ -94,8 +100,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
/**
* List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
- * <p>Once an emulator has connected, {@link DelayedLaunchInfo#mDevice} is set and the
- * DelayedLaunchInfo object is moved to {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
+ * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
+ * DelayedLaunchInfo object is moved to
+ * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
* <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
*/
private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
@@ -236,7 +243,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);
@@ -307,7 +314,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
* <code>DEBUG_MODE</code>.
* @param apk the resource to the apk to launch.
* @param debuggable the debuggable value of the app, or null if not set.
- * @param requiredApiVersionNumber the api version required by the app, or -1 if none.
+ * @param requiredApiVersionNumber the api version required by the app, or
+ * {@link AndroidManifestParser#INVALID_MIN_SDK} if none.
* @param launchAction the action to perform after app sync
* @param config the launch configuration
* @param launch the launch object
@@ -331,6 +339,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);
@@ -355,7 +373,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();
@@ -468,7 +486,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
// FIXME: ask the user if he wants to create a AVD.
// we found no compatible AVD.
AdtPlugin.printErrorToConsole(project, String.format(
- "Failed to find a AVD compatible with target '%1$s'. Launch aborted.",
+ "Failed to find an AVD compatible with target '%1$s'. Launch aborted.",
projectTarget.getName()));
stopLaunch(launchInfo);
return;
@@ -638,20 +656,21 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
String deviceApiVersionName = device.getProperty(IDevice.PROP_BUILD_VERSION);
String value = device.getProperty(IDevice.PROP_BUILD_VERSION_NUMBER);
- int deviceApiVersionNumber = 0;
+ int deviceApiVersionNumber = AndroidManifestParser.INVALID_MIN_SDK;
try {
deviceApiVersionNumber = Integer.parseInt(value);
} catch (NumberFormatException e) {
// pass, we'll keep the deviceVersionNumber value at 0.
}
- if (launchInfo.getRequiredApiVersionNumber() == 0) {
+ if (launchInfo.getRequiredApiVersionNumber() == AndroidManifestParser.INVALID_MIN_SDK) {
// warn the API level requirement is not set.
AdtPlugin.printErrorToConsole(launchInfo.getProject(),
"WARNING: Application does not specify an API level requirement!");
// and display the target device API level (if known)
- if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+ if (deviceApiVersionName == null ||
+ deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
AdtPlugin.printErrorToConsole(launchInfo.getProject(),
"WARNING: Unknown device API version!");
} else {
@@ -660,7 +679,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener
deviceApiVersionName));
}
} else { // app requires a specific API level
- if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+ if (deviceApiVersionName == null ||
+ deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) {
AdtPlugin.printToConsole(launchInfo.getProject(),
"WARNING: Unknown device API version!");
} else if (deviceApiVersionNumber < launchInfo.getRequiredApiVersionNumber()) {
@@ -792,6 +812,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;
}
@@ -804,6 +832,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<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
+ List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
+
+ // Convert to equivalent JavaProject
+ IJavaProject javaProject;
+ try {
+ //assuming this is an Android (and Java) project since it is attached to the launchInfo.
+ javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
+ } catch (CoreException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // Get all projects that this depends on
+ List<IJavaProject> androidProjectList;
+ try {
+ androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
+ } catch (JavaModelException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // for each project, parse manifest and create launch information
+ for (IJavaProject androidProject : androidProjectList) {
+ // Parse the Manifest to get various required information
+ // copied from LaunchConfigDelegate
+ AndroidManifestParser manifestParser;
+ try {
+ manifestParser = AndroidManifestParser.parse(
+ androidProject, null /* errorListener */,
+ true /* gatherData */, false /* markErrors */);
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(
+ launchInfo.getProject(),
+ String.format("Error parsing manifest of %s",
+ androidProject.getElementName()));
+ continue;
+ }
+
+ // Get the APK location (can return null)
+ IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
+ if (apk == null) {
+ // getApplicationPackage will have logged an error message
+ continue;
+ }
+
+ // Create new launchInfo as an hybrid between parent and dependency information
+ DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
+ androidProject.getProject(),
+ manifestParser.getPackage(),
+ launchInfo.getLaunchAction(),
+ apk,
+ manifestParser.getDebuggable(),
+ manifestParser.getApiLevelRequirement(),
+ launchInfo.getLaunch(),
+ launchInfo.getMonitor());
+
+ // Add to the list
+ dependencies.add(delayedLaunchInfo);
+ }
+
+ return dependencies;
+ }
+
+
+
+ /**
* Installs the application package that was pushed to a temporary location on the device.
* @param launchInfo The launch information
* @param remotePath The remote path of the package.
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
index a59518cd7..7dae56d00 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java
@@ -16,12 +16,13 @@
package com.android.ide.eclipse.adt.launch;
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
-import com.android.ddmlib.IDevice;
-
/**
* A delayed launch waiting for a device to be present or ready before the
* application is launched.
@@ -50,7 +51,8 @@ public final class DelayedLaunchInfo {
/** debuggable attribute of the manifest file. */
private final Boolean mDebuggable;
- /** Required ApiVersionNumber by the app. 0 means no requirements */
+ /** Required ApiVersionNumber by the app. {@link AndroidManifestParser#INVALID_MIN_SDK} means
+ * no requirements */
private final int mRequiredApiVersionNumber;
private InstallRetryMode mRetryMode = InstallRetryMode.NEVER;
@@ -81,7 +83,8 @@ public final class DelayedLaunchInfo {
* @param launchAction action to perform after app install
* @param pack IFile to the package (.apk) file
* @param debuggable debuggable attribute of the app's manifest file.
- * @param requiredApiVersionNumber required SDK version by the app. 0 means no requirements.
+ * @param requiredApiVersionNumber required SDK version by the app.
+ * {@link AndroidManifestParser#INVALID_MIN_SDK} means no requirements.
* @param launch the launch object
* @param monitor progress monitor for launch
*/
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java
index b898f63c5..3789153e4 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java
+++ b/tools/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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java
index 80f62eaa8..d057ac709 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java
+++ b/tools/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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
index 599da5f51..91bd21cb7 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java
@@ -55,6 +55,11 @@ import org.eclipse.swt.widgets.Text;
*/
public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
+ /**
+ *
+ */
+ public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab.png"; //$NON-NLS-1$
+
protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
protected Text mProjText;
@@ -194,7 +199,7 @@ public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
@Override
public Image getImage() {
- return AdtPlugin.getImageLoader().loadImage("mainLaunchTab.png", null);
+ return AdtPlugin.getImageLoader().loadImage(LAUNCH_TAB_IMAGE, null);
}
@@ -310,21 +315,8 @@ public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
}
mProjText.setText(projectName);
- // get the list of projects
- IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
-
- if (projects != null) {
- // look for the currently selected project
- IProject proj = null;
- for (IJavaProject p : projects) {
- if (p.getElementName().equals(projectName)) {
- proj = p.getProject();
- break;
- }
- }
-
- loadActivities(proj);
- }
+ IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+ loadActivities(proj);
// load the launch action.
mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java
new file mode 100644
index 000000000..747fcfe5c
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchAction.java
@@ -0,0 +1,276 @@
+/*
+ * 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.launch.junit;
+
+import com.android.ddmlib.IDevice;
+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 org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IProcess;
+import org.eclipse.debug.core.model.IStreamsProxy;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
+import org.eclipse.jdt.launching.IVMRunner;
+import org.eclipse.jdt.launching.VMRunnerConfiguration;
+
+/**
+ * A launch action that executes a instrumentation test run on an Android device.
+ */
+class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
+
+ private String mTestPackage;
+ private String mRunner;
+
+ /**
+ * Creates a AndroidJUnitLaunchAction.
+ *
+ * @param testPackage the Android application package that contains the tests to run
+ * @param runner the InstrumentationTestRunner that will execute the tests
+ */
+ public AndroidJUnitLaunchAction(String testPackage, String runner) {
+ mTestPackage = testPackage;
+ mRunner = runner;
+ }
+
+ /**
+ * Launch a instrumentation test run on given Android device.
+ * Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
+ *
+ * @see IAndroidLaunchAction#doLaunchAction(DelayedLaunchInfo, IDevice)
+ */
+ public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
+ String msg = String.format("Launching instrumentation %s on device %s", mRunner,
+ device.getSerialNumber());
+ AdtPlugin.printToConsole(info.getProject(), msg);
+
+ try {
+ JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(info, device);
+ final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE :
+ ILaunchManager.RUN_MODE;
+ junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
+ info.getMonitor());
+
+ // TODO: need to add AMReceiver-type functionality somewhere
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(info.getProject(), "Failed to launch test");
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getLaunchDescription() {
+ return String.format("%s JUnit launch", mRunner);
+ }
+
+ /**
+ * Extends the JDT JUnit launch delegate to allow for JUnit UI reuse.
+ */
+ private class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
+
+ private IDevice mDevice;
+ private DelayedLaunchInfo mLaunchInfo;
+
+ public JUnitLaunchDelegate(DelayedLaunchInfo info, IDevice device) {
+ mLaunchInfo = info;
+ mDevice = device;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
+ */
+ @Override
+ public synchronized void launch(ILaunchConfiguration configuration, String mode,
+ ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ // TODO: is progress monitor adjustment needed here?
+ super.launch(configuration, mode, launch, monitor);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public String verifyMainTypeName(ILaunchConfiguration configuration) throws CoreException {
+ return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
+ }
+
+ /**
+ * Overrides parent to return a VM Runner implementation which launches a thread, rather
+ * than a separate VM process
+ * @throws CoreException
+ */
+ @Override
+ public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode)
+ throws CoreException {
+ return new VMTestRunner(new AndroidJUnitLaunchInfo(mLaunchInfo.getProject(),
+ mTestPackage, mRunner, mLaunchInfo.isDebugMode(), mDevice));
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
+ */
+ @Override
+ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
+ throws CoreException {
+ return mLaunchInfo.getLaunch();
+ }
+ }
+
+ /**
+ * Provides a VM runner implementation which starts a thread implementation of a launch process
+ */
+ private static class VMTestRunner implements IVMRunner {
+
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+
+ VMTestRunner(AndroidJUnitLaunchInfo info) {
+ mJUnitInfo = info;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ public void run(final VMRunnerConfiguration config, ILaunch launch,
+ IProgressMonitor monitor) throws CoreException {
+
+ TestRunnerProcess runnerProcess =
+ new TestRunnerProcess(config, launch, mJUnitInfo);
+ runnerProcess.start();
+ launch.addProcess(runnerProcess);
+ }
+ }
+
+ /**
+ * Launch process that executes the tests.
+ */
+ private static class TestRunnerProcess extends Thread implements IProcess {
+
+ private final VMRunnerConfiguration mRunConfig;
+ private final ILaunch mLaunch;
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+ private RemoteAdtTestRunner mTestRunner = null;
+ private boolean mIsTerminated = false;
+
+ TestRunnerProcess(VMRunnerConfiguration runConfig, ILaunch launch,
+ AndroidJUnitLaunchInfo info) {
+ mRunConfig = runConfig;
+ mLaunch = launch;
+ mJUnitInfo = info;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
+ */
+ public String getAttribute(String key) {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws DebugException
+ * @see org.eclipse.debug.core.model.IProcess#getExitValue()
+ */
+ public int getExitValue() throws DebugException {
+ return 0;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLabel()
+ */
+ public String getLabel() {
+ return mLaunch.getLaunchMode();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLaunch()
+ */
+ public ILaunch getLaunch() {
+ return mLaunch;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
+ */
+ public IStreamsProxy getStreamsProxy() {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String,
+ * java.lang.String)
+ */
+ public void setAttribute(String key, String value) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
+ */
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
+ */
+ public boolean canTerminate() {
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
+ */
+ public boolean isTerminated() {
+ return mIsTerminated;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws DebugException
+ * @see org.eclipse.debug.core.model.ITerminate#terminate()
+ */
+ public void terminate() throws DebugException {
+ if (mTestRunner != null) {
+ mTestRunner.terminate();
+ }
+ mIsTerminated = true;
+ }
+
+ /**
+ * Launches a test runner that will communicate results back to JDT JUnit UI
+ */
+ @Override
+ public void run() {
+ mTestRunner = new RemoteAdtTestRunner();
+ mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
+ }
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java
new file mode 100755
index 000000000..fa8e4b01a
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigDelegate.java
@@ -0,0 +1,134 @@
+/*
+ * 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.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.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.IProject;
+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.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+
+/**
+ * Run configuration that can execute JUnit tests on an Android platform.
+ * <p/>
+ * Will deploy apps on target Android platform by reusing functionality from ADT
+ * LaunchConfigDelegate, and then run JUnits tests by reusing functionality from JDT
+ * JUnitLaunchConfigDelegate.
+ */
+@SuppressWarnings("restriction")
+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$
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+ @Override
+ protected void doLaunch(final ILaunchConfiguration configuration, final String mode,
+ IProgressMonitor monitor, IProject project, final AndroidLaunch androidLaunch,
+ AndroidLaunchConfiguration config, AndroidLaunchController controller,
+ IFile applicationPackage, AndroidManifestParser manifestParser) {
+
+ String testPackage = manifestParser.getPackage();
+ String runner = getRunner(project, configuration, manifestParser);
+ if (runner == null) {
+ AdtPlugin.displayError("Android Launch",
+ "An instrumention test runner is not specified!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ IAndroidLaunchAction junitLaunch = new AndroidJUnitLaunchAction(testPackage, runner);
+
+ controller.launch(project, mode, applicationPackage, manifestParser.getPackage(),
+ manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(),
+ junitLaunch, config, androidLaunch, monitor);
+ }
+
+ /**
+ * Gets a instrumentation runner for the launch.
+ * <p/>
+ * If a runner is stored in the given <code>configuration</code>, will return that.
+ * Otherwise, will try to find the first valid runner for the project.
+ * If a runner can still not be found, will return <code>null</code>.
+ *
+ * @param project the {@link IProject} for the app
+ * @param configuration the {@link ILaunchConfiguration} for the launch
+ * @param manifestParser the {@link AndroidManifestParser} for the project
+ *
+ * @return <code>null</code> if no instrumentation runner can be found, otherwise return
+ * the fully qualified runner name.
+ */
+ private String getRunner(IProject project, ILaunchConfiguration configuration,
+ AndroidManifestParser manifestParser) {
+ try {
+ String runner = getRunnerFromConfig(configuration);
+ if (runner != null) {
+ return runner;
+ }
+ final InstrumentationRunnerValidator instrFinder = new InstrumentationRunnerValidator(
+ BaseProjectHelper.getJavaProject(project), manifestParser);
+ runner = instrFinder.getValidInstrumentationTestRunner();
+ if (runner != null) {
+ AdtPlugin.printErrorToConsole(project,
+ String.format("Warning: No instrumentation runner found for the launch, " +
+ "using %1$s", runner));
+ return runner;
+ }
+ AdtPlugin.printErrorToConsole(project,
+ String.format("ERROR: Application does not specify a %1$s instrumentation or does not declare uses-library %2$s",
+ AndroidConstants.CLASS_INSTRUMENTATION_RUNNER,
+ AndroidConstants.LIBRARY_TEST_RUNNER));
+ return null;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Error when retrieving instrumentation info"); //$NON-NLS-1$
+ }
+ return null;
+
+ }
+
+ private String getRunnerFromConfig(ILaunchConfiguration configuration) throws CoreException {
+ String runner = configuration.getAttribute(ATTR_INSTR_NAME, EMPTY_STRING);
+ if (runner.length() < 1) {
+ return null;
+ }
+ return runner;
+ }
+
+ /**
+ * Helper method to set JUnit-related attributes expected by JDT JUnit runner
+ *
+ * @param config the launch configuration to modify
+ */
+ static void setJUnitDefaults(ILaunchConfigurationWorkingCopy config) {
+ // set the test runner to JUnit3 to placate JDT JUnit runner logic
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java
new file mode 100644
index 000000000..eb5748269
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchConfigurationTab.java
@@ -0,0 +1,977 @@
+/*
+ * 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.launch.junit;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.launch.MainLaunchConfigTab;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.ISourceReference;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.Messages;
+import org.eclipse.jdt.internal.junit.launcher.ITestKind;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.JUnitMigrationDelegate;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.internal.junit.launcher.TestSelectionDialog;
+import org.eclipse.jdt.internal.junit.ui.JUnitMessages;
+import org.eclipse.jdt.internal.junit.util.LayoutUtil;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator;
+import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.ui.JavaElementComparator;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
+import org.eclipse.ui.dialogs.SelectionDialog;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * The launch config UI tab for Android JUnit
+ * <p/>
+ * Based on org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationTab
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab {
+
+ // Project UI widgets
+ private Label mProjLabel;
+ private Text mProjText;
+ private Button mProjButton;
+
+ // Test class UI widgets
+ private Text mTestText;
+ private Button mSearchButton;
+ private String mOriginalTestMethodName;
+ private Label mTestMethodLabel;
+ private Text mContainerText;
+ private IJavaElement mContainerElement;
+ private final ILabelProvider mJavaElementLabelProvider = new JavaElementLabelProvider();
+
+ private Button mContainerSearchButton;
+ private Button mTestContainerRadioButton;
+ private Button mTestRadioButton;
+ private Label mTestLabel;
+
+ // Android specific members
+ private Image mTabIcon = null;
+ private Combo mInstrumentationCombo;
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+ private static final String TAG = "AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ private String[] mInstrumentations = null;
+ private InstrumentationRunnerValidator mInstrValidator = null;
+ private ProjectChooserHelper mProjectChooserHelper;
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
+
+ Composite comp = new Composite(parent, SWT.NONE);
+ setControl(comp);
+
+ GridLayout topLayout = new GridLayout();
+ topLayout.numColumns = 3;
+ comp.setLayout(topLayout);
+
+ createSingleTestSection(comp);
+ createTestContainerSelectionGroup(comp);
+
+ createSpacer(comp);
+
+ createInstrumentationGroup(comp);
+
+ createSpacer(comp);
+
+ Dialog.applyDialogFont(comp);
+ // TODO: add help link here when available
+ //PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(),
+ // IJUnitHelpContextIds.LAUNCH_CONFIGURATION_DIALOG_JUNIT_MAIN_TAB);
+ validatePage();
+ }
+
+
+ private void createSpacer(Composite comp) {
+ Label label = new Label(comp, SWT.NONE);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ label.setLayoutData(gd);
+ }
+
+ private void createSingleTestSection(Composite comp) {
+ mTestRadioButton = new Button(comp, SWT.RADIO);
+ mTestRadioButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_oneTest);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestRadioButton.setLayoutData(gd);
+ mTestRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ });
+
+ mProjLabel = new Label(comp, SWT.NONE);
+ mProjLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_project);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mProjLabel.setLayoutData(gd);
+
+ mProjText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ mSearchButton.setEnabled(mTestRadioButton.getSelection() &&
+ mProjText.getText().length() > 0);
+ }
+ });
+
+ mProjButton = new Button(comp, SWT.PUSH);
+ mProjButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_browse);
+ mProjButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleProjectButtonSelected();
+ }
+ });
+ setButtonGridData(mProjButton);
+
+ mTestLabel = new Label(comp, SWT.NONE);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mTestLabel.setLayoutData(gd);
+ mTestLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_test);
+
+
+ mTestText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTestText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mSearchButton = new Button(comp, SWT.PUSH);
+ mSearchButton.setEnabled(mProjText.getText().length() > 0);
+ mSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mSearchButton);
+
+ new Label(comp, SWT.NONE);
+
+ mTestMethodLabel = new Label(comp, SWT.NONE);
+ mTestMethodLabel.setText(""); //$NON-NLS-1$
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ mTestMethodLabel.setLayoutData(gd);
+ }
+
+ private void createTestContainerSelectionGroup(Composite comp) {
+ mTestContainerRadioButton = new Button(comp, SWT.RADIO);
+ mTestContainerRadioButton.setText(
+ JUnitMessages.JUnitLaunchConfigurationTab_label_containerTest);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestContainerRadioButton.setLayoutData(gd);
+ mTestContainerRadioButton.addSelectionListener(new SelectionListener() {
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestContainerRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+ });
+
+ mContainerText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 25;
+ gd.horizontalSpan = 2;
+ mContainerText.setLayoutData(gd);
+ mContainerText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent evt) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mContainerSearchButton = new Button(comp, SWT.PUSH);
+ mContainerSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mContainerSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleContainerSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mContainerSearchButton);
+ }
+
+ private void createInstrumentationGroup(Composite comp) {
+ Label loaderLabel = new Label(comp, SWT.NONE);
+ loaderLabel.setText("Instrumentation runner:");
+ GridData gd = new GridData();
+ gd.horizontalIndent = 0;
+ loaderLabel.setLayoutData(gd);
+
+ mInstrumentationCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mInstrumentationCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mInstrumentationCombo.clearSelection();
+ mInstrumentationCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void handleContainerSearchButtonSelected() {
+ IJavaElement javaElement = chooseContainer(mContainerElement);
+ if (javaElement != null) {
+ setContainerElement(javaElement);
+ }
+ }
+
+ private void setContainerElement(IJavaElement javaElement) {
+ mContainerElement = javaElement;
+ mContainerText.setText(getPresentationName(javaElement));
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ public void initializeFrom(ILaunchConfiguration config) {
+ String projectName = updateProjectFromConfig(config);
+ String containerHandle = EMPTY_STRING;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+
+ if (containerHandle.length() > 0) {
+ updateTestContainerFromConfig(config);
+ } else {
+ updateTestTypeFromConfig(config);
+ }
+
+ IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+ loadInstrumentations(proj);
+ updateInstrumentationFromConfig(config);
+
+ validatePage();
+ }
+
+ private void updateInstrumentationFromConfig(ILaunchConfiguration config) {
+ boolean found = false;
+ try {
+ String currentInstrumentation = config.getAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, EMPTY_STRING);
+ if (mInstrumentations != null) {
+ // look for the name of the instrumentation in the combo.
+ for (int i = 0; i < mInstrumentations.length; i++) {
+ if (currentInstrumentation.equals(mInstrumentations[i])) {
+ found = true;
+ mInstrumentationCombo.select(i);
+ break;
+ }
+ }
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (!found) {
+ mInstrumentationCombo.clearSelection();
+ }
+ }
+
+ private String updateProjectFromConfig(ILaunchConfiguration config) {
+ String projectName = EMPTY_STRING;
+ try {
+ projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mProjText.setText(projectName);
+ return projectName;
+ }
+
+ private void updateTestTypeFromConfig(ILaunchConfiguration config) {
+ String testTypeName = EMPTY_STRING;
+ mOriginalTestMethodName = EMPTY_STRING;
+ try {
+ testTypeName = config.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
+ mOriginalTestMethodName = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mTestRadioButton.setSelection(true);
+ setEnableSingleTestGroup(true);
+ setEnableContainerTestGroup(false);
+ mTestContainerRadioButton.setSelection(false);
+ mTestText.setText(testTypeName);
+ mContainerText.setText(EMPTY_STRING);
+ setTestMethodLabel(mOriginalTestMethodName);
+ }
+
+ private void setTestMethodLabel(String testMethodName) {
+ if (!EMPTY_STRING.equals(testMethodName)) {
+ mTestMethodLabel.setText(
+ JUnitMessages.JUnitLaunchConfigurationTab_label_method +
+ mOriginalTestMethodName);
+ } else {
+ mTestMethodLabel.setText(EMPTY_STRING);
+ }
+ }
+
+ private void updateTestContainerFromConfig(ILaunchConfiguration config) {
+ String containerHandle = EMPTY_STRING;
+ IJavaElement containerElement = null;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ if (containerHandle.length() > 0) {
+ containerElement = JavaCore.create(containerHandle);
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (containerElement != null) {
+ mContainerElement = containerElement;
+ }
+ mTestContainerRadioButton.setSelection(true);
+ setEnableSingleTestGroup(false);
+ setEnableContainerTestGroup(true);
+ mTestRadioButton.setSelection(false);
+ if (mContainerElement != null) {
+ mContainerText.setText(getPresentationName(mContainerElement));
+ }
+ mTestText.setText(EMPTY_STRING);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ public void performApply(ILaunchConfigurationWorkingCopy config) {
+ if (mTestContainerRadioButton.getSelection() && mContainerElement != null) {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mContainerElement.getJavaProject().getElementName());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ mContainerElement.getHandleIdentifier());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ EMPTY_STRING);
+ //workaround for Eclipse bug 65399
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ EMPTY_STRING);
+ } else {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mProjText.getText());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ mTestText.getText());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ mOriginalTestMethodName);
+ }
+ try {
+ mapResources(config);
+ } catch (CoreException e) {
+ // TODO: does the real error need to be extracted out of CoreException
+ AdtPlugin.log(e, "Error occurred saving configuration"); //$NON-NLS-1$
+ }
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ getSelectedInstrumentation());
+ }
+
+ private void mapResources(ILaunchConfigurationWorkingCopy config) throws CoreException {
+ JUnitMigrationDelegate.mapResources(config);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#dispose()
+ */
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (mTabIcon != null) {
+ mTabIcon.dispose();
+ mTabIcon = null;
+ }
+ mJavaElementLabelProvider.dispose();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getImage()
+ */
+ @Override
+ public Image getImage() {
+ // reuse icon from the Android App Launch config tab
+ if (mTabIcon == null) {
+ mTabIcon = AdtPlugin.getImageLoader().loadImage(MainLaunchConfigTab.LAUNCH_TAB_IMAGE,
+ null);
+ }
+ return mTabIcon;
+ }
+
+ /**
+ * Show a dialog that lists all main types
+ */
+ private void handleSearchButtonSelected() {
+ Shell shell = getShell();
+
+ IJavaProject javaProject = getJavaProject();
+
+ IType[] types = new IType[0];
+ boolean[] radioSetting = new boolean[2];
+ try {
+ // fix for Eclipse bug 66922 Wrong radio behaviour when switching
+ // remember the selected radio button
+ radioSetting[0] = mTestRadioButton.getSelection();
+ radioSetting[1] = mTestContainerRadioButton.getSelection();
+
+ types = TestSearchEngine.findTests(getLaunchConfigurationDialog(), javaProject,
+ getTestKind());
+ } catch (InterruptedException e) {
+ setErrorMessage(e.getMessage());
+ return;
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e.getTargetException(), "Error finding test types"); //$NON-NLS-1$
+ return;
+ } finally {
+ mTestRadioButton.setSelection(radioSetting[0]);
+ mTestContainerRadioButton.setSelection(radioSetting[1]);
+ }
+
+ SelectionDialog dialog = new TestSelectionDialog(shell, types);
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_message);
+ if (dialog.open() == Window.CANCEL) {
+ return;
+ }
+
+ Object[] results = dialog.getResult();
+ if ((results == null) || (results.length < 1)) {
+ return;
+ }
+ IType type = (IType) results[0];
+
+ if (type != null) {
+ mTestText.setText(type.getFullyQualifiedName('.'));
+ javaProject = type.getJavaProject();
+ mProjText.setText(javaProject.getElementName());
+ }
+ }
+
+ private ITestKind getTestKind() {
+ // harddcode this to JUnit 3
+ return TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+
+ /**
+ * Show a dialog that lets the user select a Android project. This in turn provides
+ * context for the main type, allowing the user to key a main type name, or
+ * constraining the search for main types to the specified project.
+ */
+ private void handleProjectButtonSelected() {
+ IJavaProject project = mProjectChooserHelper.chooseJavaProject(getProjectName());
+ if (project == null) {
+ return;
+ }
+
+ String projectName = project.getElementName();
+ mProjText.setText(projectName);
+ loadInstrumentations(project.getProject());
+ }
+
+ /**
+ * Return the IJavaProject corresponding to the project name in the project name
+ * text field, or null if the text does not match a Android project name.
+ */
+ private IJavaProject getJavaProject() {
+ String projectName = getProjectName();
+ return getJavaModel().getJavaProject(projectName);
+ }
+
+ /**
+ * Returns the name of the currently specified project. Null if no project is selected.
+ */
+ private String getProjectName() {
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() < 1) {
+ return null;
+ }
+ return projectName;
+ }
+
+ /**
+ * Convenience method to get the workspace root.
+ */
+ private IWorkspaceRoot getWorkspaceRoot() {
+ return ResourcesPlugin.getWorkspace().getRoot();
+ }
+
+ /**
+ * Convenience method to get access to the java model.
+ */
+ private IJavaModel getJavaModel() {
+ return JavaCore.create(getWorkspaceRoot());
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#isValid(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public boolean isValid(ILaunchConfiguration config) {
+ validatePage();
+ return getErrorMessage() == null;
+ }
+
+ private void testModeChanged() {
+ boolean isSingleTestMode = mTestRadioButton.getSelection();
+ setEnableSingleTestGroup(isSingleTestMode);
+ setEnableContainerTestGroup(!isSingleTestMode);
+ if (!isSingleTestMode && mContainerText.getText().length() == 0) {
+ String projText = mProjText.getText();
+ if (Path.EMPTY.isValidSegment(projText)) {
+ IJavaProject javaProject = getJavaModel().getJavaProject(projText);
+ if (javaProject != null && javaProject.exists()) {
+ setContainerElement(javaProject);
+ }
+ }
+ }
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ private void validatePage() {
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mTestContainerRadioButton.getSelection()) {
+ if (mContainerElement == null) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_noContainer);
+ return;
+ }
+ validateJavaProject(mContainerElement.getJavaProject());
+ return;
+ }
+
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotdefined);
+ return;
+ }
+
+ IStatus status = ResourcesPlugin.getWorkspace().validatePath(IPath.SEPARATOR + projectName,
+ IResource.PROJECT);
+ if (!status.isOK() || !Path.ROOT.isValidSegment(projectName)) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_invalidProjectName,
+ projectName));
+ return;
+ }
+
+ IProject project = getWorkspaceRoot().getProject(projectName);
+ if (!project.exists()) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotexists);
+ return;
+ }
+ IJavaProject javaProject = JavaCore.create(project);
+ validateJavaProject(javaProject);
+
+ try {
+ if (!project.hasNature(AndroidConstants.NATURE)) {
+ setErrorMessage("Specified project is not an Android project");
+ return;
+ }
+ String className = mTestText.getText().trim();
+ if (className.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
+ return;
+ }
+ if (javaProject.findType(className) == null) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found,
+ new String[] { className, projectName }));
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "validatePage failed"); //$NON-NLS-1$
+ }
+
+ validateInstrumentation();
+ }
+
+ private void validateJavaProject(IJavaProject javaProject) {
+ if (!TestSearchEngine.hasTestCaseType(javaProject)) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testcasenotonpath);
+ return;
+ }
+ }
+
+ private void validateInstrumentation() {
+ String instrumentation = getSelectedInstrumentation();
+ if (instrumentation == null) {
+ setErrorMessage("Instrumentation runner not specified");
+ return;
+ }
+ String result = mInstrValidator.validateInstrumentationRunner(instrumentation);
+ if (result != InstrumentationRunnerValidator.INSTRUMENTATION_OK) {
+ setErrorMessage(result);
+ return;
+ }
+ }
+
+ private String getSelectedInstrumentation() {
+ int selectionIndex = mInstrumentationCombo.getSelectionIndex();
+ if (mInstrumentations != null && selectionIndex >= 0 &&
+ selectionIndex < mInstrumentations.length) {
+ return mInstrumentations[selectionIndex];
+ }
+ return null;
+ }
+
+ private void setEnableContainerTestGroup(boolean enabled) {
+ mContainerSearchButton.setEnabled(enabled);
+ mContainerText.setEnabled(enabled);
+ }
+
+ private void setEnableSingleTestGroup(boolean enabled) {
+ mProjLabel.setEnabled(enabled);
+ mProjText.setEnabled(enabled);
+ mProjButton.setEnabled(enabled);
+ mTestLabel.setEnabled(enabled);
+ mTestText.setEnabled(enabled);
+ mSearchButton.setEnabled(enabled && mProjText.getText().length() > 0);
+ mTestMethodLabel.setEnabled(enabled);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ public void setDefaults(ILaunchConfigurationWorkingCopy config) {
+ IJavaElement javaElement = getContext();
+ if (javaElement != null) {
+ initializeJavaProject(javaElement, config);
+ } else {
+ // We set empty attributes for project & main type so that when one config is
+ // compared to another, the existence of empty attributes doesn't cause an
+ // incorrect result (the performApply() method can result in empty values
+ // for these attributes being set on a config if there is nothing in the
+ // corresponding text boxes)
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ }
+ initializeTestAttributes(javaElement, config);
+ }
+
+ private void initializeTestAttributes(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ if (javaElement != null && javaElement.getElementType() < IJavaElement.COMPILATION_UNIT) {
+ initializeTestContainer(javaElement, config);
+ } else {
+ initializeTestType(javaElement, config);
+ }
+ }
+
+ private void initializeTestContainer(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ javaElement.getHandleIdentifier());
+ initializeName(config, javaElement.getElementName());
+ }
+
+ private void initializeName(ILaunchConfigurationWorkingCopy config, String name) {
+ if (name == null) {
+ name = EMPTY_STRING;
+ }
+ if (name.length() > 0) {
+ int index = name.lastIndexOf('.');
+ if (index > 0) {
+ name = name.substring(index + 1);
+ }
+ name = getLaunchConfigurationDialog().generateName(name);
+ config.rename(name);
+ }
+ }
+
+ /**
+ * Sets the main type & name attributes on the working copy based on the IJavaElement
+ */
+ private void initializeTestType(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ String name = EMPTY_STRING;
+ String testKindId = null;
+ try {
+ // only do a search for compilation units or class files or source references
+ if (javaElement instanceof ISourceReference) {
+ ITestKind testKind = TestKindRegistry.getContainerTestKind(javaElement);
+ testKindId = testKind.getId();
+
+ IType[] types = TestSearchEngine.findTests(getLaunchConfigurationDialog(),
+ javaElement, testKind);
+ if ((types == null) || (types.length < 1)) {
+ return;
+ }
+ // Simply grab the first main type found in the searched element
+ name = types[0].getFullyQualifiedName('.');
+
+ }
+ } catch (InterruptedException ie) {
+ // ignore
+ } catch (InvocationTargetException ite) {
+ // ignore
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, name);
+ if (testKindId != null) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ testKindId);
+ }
+ initializeName(config, name);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+ */
+ public String getName() {
+ return JUnitMessages.JUnitLaunchConfigurationTab_tab_label;
+ }
+
+ @SuppressWarnings("unchecked")
+ private IJavaElement chooseContainer(IJavaElement initElement) {
+ Class[] acceptedClasses = new Class[] { IPackageFragmentRoot.class, IJavaProject.class,
+ IPackageFragment.class };
+ TypedElementSelectionValidator validator = new TypedElementSelectionValidator(
+ acceptedClasses, false) {
+ @Override
+ public boolean isSelectedValid(Object element) {
+ return true;
+ }
+ };
+
+ acceptedClasses = new Class[] { IJavaModel.class, IPackageFragmentRoot.class,
+ IJavaProject.class, IPackageFragment.class };
+ ViewerFilter filter = new TypedViewerFilter(acceptedClasses) {
+ @Override
+ public boolean select(Viewer viewer, Object parent, Object element) {
+ if (element instanceof IPackageFragmentRoot &&
+ ((IPackageFragmentRoot) element).isArchive()) {
+ return false;
+ }
+ try {
+ if (element instanceof IPackageFragment &&
+ !((IPackageFragment) element).hasChildren()) {
+ return false;
+ }
+ } catch (JavaModelException e) {
+ return false;
+ }
+ return super.select(viewer, parent, element);
+ }
+ };
+
+ StandardJavaElementContentProvider provider = new StandardJavaElementContentProvider();
+ ILabelProvider labelProvider = new JavaElementLabelProvider(
+ JavaElementLabelProvider.SHOW_DEFAULT);
+ ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(),
+ labelProvider, provider);
+ dialog.setValidator(validator);
+ dialog.setComparator(new JavaElementComparator());
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_message);
+ dialog.addFilter(filter);
+ dialog.setInput(JavaCore.create(getWorkspaceRoot()));
+ dialog.setInitialSelection(initElement);
+ dialog.setAllowMultiple(false);
+
+ if (dialog.open() == Window.OK) {
+ Object element = dialog.getFirstResult();
+ return (IJavaElement) element;
+ }
+ return null;
+ }
+
+ private String getPresentationName(IJavaElement element) {
+ return mJavaElementLabelProvider.getText(element);
+ }
+
+ /**
+ * Returns the current Java element context from which to initialize
+ * default settings, or <code>null</code> if none.
+ *
+ * @return Java element context.
+ */
+ private IJavaElement getContext() {
+ IWorkbenchWindow activeWorkbenchWindow =
+ PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (activeWorkbenchWindow == null) {
+ return null;
+ }
+ IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
+ if (page != null) {
+ ISelection selection = page.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection ss = (IStructuredSelection) selection;
+ if (!ss.isEmpty()) {
+ Object obj = ss.getFirstElement();
+ if (obj instanceof IJavaElement) {
+ return (IJavaElement) obj;
+ }
+ if (obj instanceof IResource) {
+ IJavaElement je = JavaCore.create((IResource) obj);
+ if (je == null) {
+ IProject pro = ((IResource) obj).getProject();
+ je = JavaCore.create(pro);
+ }
+ if (je != null) {
+ return je;
+ }
+ }
+ }
+ }
+ IEditorPart part = page.getActiveEditor();
+ if (part != null) {
+ IEditorInput input = part.getEditorInput();
+ return (IJavaElement) input.getAdapter(IJavaElement.class);
+ }
+ }
+ return null;
+ }
+
+ private void initializeJavaProject(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ IJavaProject javaProject = javaElement.getJavaProject();
+ String name = null;
+ if (javaProject != null && javaProject.exists()) {
+ name = javaProject.getElementName();
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+ }
+
+ private void setButtonGridData(Button button) {
+ GridData gridData = new GridData();
+ button.setLayoutData(gridData);
+ LayoutUtil.setButtonDimensionHint(button);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getId()
+ */
+ @Override
+ public String getId() {
+ return "com.android.ide.eclipse.adt.launch.AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ }
+
+ /**
+ * Loads the UI with the instrumentations of the specified project, and stores the
+ * instrumentations in <code>mInstrumentations</code>.
+ *
+ * @param project the {@link IProject} to load the instrumentations from.
+ */
+ private void loadInstrumentations(IProject project) {
+ try {
+ mInstrValidator = new InstrumentationRunnerValidator(project);
+ mInstrumentations = (mInstrValidator == null ? null :
+ mInstrValidator.getInstrumentations());
+ if (mInstrumentations != null) {
+ mInstrumentationCombo.removeAll();
+ for (String instrumentation : mInstrumentations) {
+ mInstrumentationCombo.add(instrumentation);
+ }
+ // the selection will be set when we update the ui from the current
+ // config object.
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.logAndPrintError(e, TAG, "ERROR: Failed to get instrumentations for %1$s",
+ project.getName());
+ }
+ // if we reach this point, either project is null, or we got an exception during
+ // the parsing. In either case, we empty the instrumentation list.
+ mInstrValidator = null;
+ mInstrumentations = null;
+ mInstrumentationCombo.removeAll();
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java
new file mode 100755
index 000000000..f06f7eb4c
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitLaunchShortcut.java
@@ -0,0 +1,56 @@
+/*
+ * 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.launch.junit;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
+
+/**
+ * Launch shortcut to launch debug/run Android JUnit configuration directly.
+ */
+public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
+
+ @Override
+ protected String getLaunchConfigurationTypeId() {
+ return "com.android.ide.eclipse.adt.junit.launchConfigurationType"; //$NON-NLS-1$
+ }
+
+ /**
+ * Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
+ * first instrumentation found in the AndroidManifest.
+ */
+ @Override
+ protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
+ throws CoreException {
+ ILaunchConfigurationWorkingCopy config = super.createLaunchConfiguration(element);
+ // just get first valid instrumentation runner
+ String instrumentation = new InstrumentationRunnerValidator(element.getJavaProject()).
+ getValidInstrumentationTestRunner();
+ if (instrumentation != null) {
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ instrumentation);
+ }
+ // if a valid runner is not found, rely on launch delegate to log error.
+ // This method is called without explicit user action to launch Android JUnit, so avoid
+ // logging an error here.
+
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+ return config;
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.java
new file mode 100644
index 000000000..3c82f5745
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/AndroidJUnitTabGroup.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.launch.junit;
+
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+import com.android.ide.eclipse.adt.launch.EmulatorConfigTab;
+
+/**
+ * Tab group object for Android JUnit launch configuration type.
+ */
+public class AndroidJUnitTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+ /**
+ * Creates the UI tabs for the Android JUnit configuration
+ */
+ public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+ ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+ new AndroidJUnitLaunchConfigurationTab(),
+ new EmulatorConfigTab(),
+ new CommonTab()
+ };
+ setTabs(tabs);
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/InstrumentationRunnerValidator.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/InstrumentationRunnerValidator.java
new file mode 100644
index 000000000..f22fc7cda
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/InstrumentationRunnerValidator.java
@@ -0,0 +1,145 @@
+/*
+ * 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.launch.junit;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+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.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+
+/**
+ * Provides validation for Android instrumentation test runner
+ */
+class InstrumentationRunnerValidator {
+ private final IJavaProject mJavaProject;
+ private String[] mInstrumentations = null;
+ private boolean mHasRunnerLibrary = false;
+
+ static final String INSTRUMENTATION_OK = null;
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject) {
+ mJavaProject = javaProject;
+ try {
+ AndroidManifestParser manifestParser = AndroidManifestParser.parse(javaProject,
+ null /* errorListener */, true /* gatherData */, false /* markErrors */);
+ init(manifestParser);
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), "ERROR: Failed to parse %1$s",
+ AndroidConstants.FN_ANDROID_MANIFEST);
+ }
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param project the {@link IProject} for the Android project to validate
+ * @throws CoreException if a fatal error occurred in initialization
+ */
+ InstrumentationRunnerValidator(IProject project) throws CoreException {
+ this(BaseProjectHelper.getJavaProject(project));
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator with an existing {@link AndroidManifestParser}
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ * @param manifestParser the {@link AndroidManifestParser} for the Android project
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject, AndroidManifestParser manifestParser) {
+ mJavaProject = javaProject;
+ init(manifestParser);
+ }
+
+ private void init(AndroidManifestParser manifestParser) {
+ mInstrumentations = manifestParser.getInstrumentations();
+ mHasRunnerLibrary = hasTestRunnerLibrary(manifestParser);
+ }
+
+ /**
+ * Helper method to determine if given manifest has a <code>AndroidConstants.LIBRARY_TEST_RUNNER
+ * </code> library reference
+ *
+ * @param manifestParser the {@link AndroidManifestParser} to search
+ * @return true if test runner library found, false otherwise
+ */
+ private boolean hasTestRunnerLibrary(AndroidManifestParser manifestParser) {
+ for (String lib : manifestParser.getUsesLibraries()) {
+ if (lib.equals(AndroidConstants.LIBRARY_TEST_RUNNER)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the set of instrumentations for the Android project.
+ *
+ * @return <code>null</code if error occurred parsing instrumentations, otherwise returns array
+ * of instrumentation class names
+ */
+ String[] getInstrumentations() {
+ return mInstrumentations;
+ }
+
+ /**
+ * Helper method to get the first instrumentation that can be used as a test runner.
+ *
+ * @return fully qualified instrumentation class name. <code>null</code> if no valid
+ * instrumentation can be found.
+ */
+ String getValidInstrumentationTestRunner() {
+ for (String instrumentation : getInstrumentations()) {
+ if (validateInstrumentationRunner(instrumentation) == INSTRUMENTATION_OK) {
+ return instrumentation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to determine if specified instrumentation can be used as a test runner
+ *
+ * @param instrumentation the instrumentation class name to validate. Assumes this
+ * instrumentation is one of {@link #getInstrumentations()}
+ * @return <code>INSTRUMENTATION_OK</code> if valid, otherwise returns error message
+ */
+ String validateInstrumentationRunner(String instrumentation) {
+ if (!mHasRunnerLibrary) {
+ return String.format("The application does not declare uses-library %1$s",
+ AndroidConstants.LIBRARY_TEST_RUNNER);
+ }
+ // 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(mJavaProject,
+ 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);
+ }
+ }
+ return INSTRUMENTATION_OK;
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java
new file mode 100644
index 000000000..89cad97ae
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidJUnitLaunchInfo.java
@@ -0,0 +1,60 @@
+/*
+ * 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.launch.junit.runtime;
+
+import org.eclipse.core.resources.IProject;
+
+import com.android.ddmlib.IDevice;
+
+/**
+ * Contains info about Android JUnit launch
+ */
+public class AndroidJUnitLaunchInfo {
+ private final IProject mProject;
+ private final String mTestPackage;
+ private final String mRunner;
+ private final boolean mDebugMode;
+ private final IDevice mDevice;
+
+ public AndroidJUnitLaunchInfo(IProject project, String testPackage, String runner,
+ boolean debugMode, IDevice device) {
+ mProject = project;
+ mTestPackage = testPackage;
+ mRunner = runner;
+ mDebugMode = debugMode;
+ mDevice = device;
+ }
+
+ public IProject getProject() {
+ return mProject;
+ }
+
+ public String getTestPackage() {
+ return mTestPackage;
+ }
+
+ public String getRunner() {
+ return mRunner;
+ }
+
+ public boolean isDebugMode() {
+ return mDebugMode;
+ }
+
+ public IDevice getDevice() {
+ return mDevice;
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java
new file mode 100644
index 000000000..9db3ef06d
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/AndroidTestReference.java
@@ -0,0 +1,63 @@
+/*
+ * 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.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.ITestIdentifier;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+
+/**
+ * Base implementation of the Eclipse {@link ITestReference} and {@link ITestIdentifier} interfaces
+ * for Android tests.
+ * <p/>
+ * Provides generic equality/hashcode services
+ */
+@SuppressWarnings("restriction") //$NON-NLS-1$
+abstract class AndroidTestReference implements ITestReference, ITestIdentifier {
+
+ /**
+ * Gets the {@link ITestIdentifier} for this test reference.
+ */
+ public ITestIdentifier getIdentifier() {
+ // this class serves as its own test identifier
+ return this;
+ }
+
+ /**
+ * Not supported.
+ */
+ public void run(TestExecution execution) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Compares {@link ITestIdentifier} using names
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ITestIdentifier) {
+ ITestIdentifier testid = (ITestIdentifier) obj;
+ return getName().equals(testid.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getName().hashCode();
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java
new file mode 100755
index 000000000..0a6a3daee
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/RemoteAdtTestRunner.java
@@ -0,0 +1,222 @@
+/*
+ * 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.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
+
+/**
+ * Supports Eclipse JUnit execution of Android tests.
+ * <p/>
+ * Communicates back to a Eclipse JDT JUnit client via a socket connection.
+ *
+ * @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
+ */
+@SuppressWarnings("restriction")
+public class RemoteAdtTestRunner extends RemoteTestRunner {
+
+ private AndroidJUnitLaunchInfo mLaunchInfo;
+ private TestExecution mExecution;
+
+ /**
+ * Initialize the JDT JUnit test runner parameters from the {@code args}.
+ *
+ * @param args name-value pair of arguments to pass to parent JUnit runner.
+ * @param launchInfo the Android specific test launch info
+ */
+ protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) {
+ defaultInit(args);
+ mLaunchInfo = launchInfo;
+ }
+
+ /**
+ * Runs a set of tests, and reports back results using parent class.
+ * <p/>
+ * JDT Unit expects to be sent data in the following sequence:
+ * <ol>
+ * <li>The total number of tests to be executed.</li>
+ * <li>The test 'tree' data about the tests to be executed, which is composed of the set of
+ * test class names, the number of tests in each class, and the names of each test in the
+ * class.</li>
+ * <li>The test execution result for each test method. Expects individual notifications of
+ * the test execution start, any failures, and the end of the test execution.</li>
+ * <li>The end of the test run, with its elapsed time.</li>
+ * </ol>
+ * <p/>
+ * In order to satisfy this, this method performs two actual Android instrumentation runs.
+ * The first is a 'log only' run that will collect the test tree data, without actually
+ * executing the tests, and send it back to JDT JUnit. The second is the actual test execution,
+ * whose results will be communicated back in real-time to JDT JUnit.
+ *
+ * @param testClassNames array of fully qualified test class names to execute. Cannot be empty.
+ * @param testName test to execute. If null, will be ignored.
+ * @param execution used to report test progress
+ */
+ @Override
+ public void runTests(String[] testClassNames, String testName, TestExecution execution) {
+ // hold onto this execution reference so it can be used to report test progress
+ mExecution = execution;
+
+ RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mLaunchInfo.getTestPackage(),
+ mLaunchInfo.getRunner(), mLaunchInfo.getDevice());
+
+ if (testClassNames != null && testClassNames.length > 0) {
+ if (testName != null) {
+ runner.setMethodName(testClassNames[0], testName);
+ } else {
+ runner.setClassNames(testClassNames);
+ }
+ }
+ // set log only to first collect test case info, so Eclipse has correct test case count/
+ // tree info
+ runner.setLogOnly(true);
+ TestCollector collector = new TestCollector();
+ runner.run(collector);
+ if (collector.getErrorMessage() != null) {
+ // error occurred during test collection.
+ reportError(collector.getErrorMessage());
+ // abort here
+ notifyTestRunEnded(0);
+ return;
+ }
+ notifyTestRunStarted(collector.getTestCaseCount());
+ collector.sendTrees(this);
+
+ // now do real execution
+ runner.setLogOnly(false);
+ if (mLaunchInfo.isDebugMode()) {
+ runner.setDebug(true);
+ }
+ runner.run(new TestRunListener());
+ }
+
+ /**
+ * Main entry method to run tests
+ *
+ * @param programArgs JDT JUnit program arguments to be processed by parent
+ * @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru
+ */
+ public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
+ init(programArgs, junitInfo);
+ run();
+ }
+
+ /**
+ * Stop the current test run.
+ */
+ public void terminate() {
+ stop();
+ }
+
+ @Override
+ protected void stop() {
+ if (mExecution != null) {
+ mExecution.stop();
+ }
+ }
+
+ private void notifyTestRunEnded(long elapsedTime) {
+ // copy from parent - not ideal, but method is private
+ sendMessage(MessageIds.TEST_RUN_END + elapsedTime);
+ flush();
+ //shutDown();
+ }
+
+ /**
+ * @param errorMessage
+ */
+ private void reportError(String errorMessage) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
+ String.format("Test run failed: %s", errorMessage));
+ // is this needed?
+ //notifyTestRunStopped(-1);
+ }
+
+ /**
+ * TestRunListener that communicates results in real-time back to JDT JUnit
+ */
+ private class TestRunListener implements ITestRunListener {
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ public void testEnded(TestIdentifier test) {
+ mExecution.getListener().notifyTestEnded(new TestCaseReference(test));
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ String statusString;
+ if (status == TestFailure.ERROR) {
+ statusString = MessageIds.TEST_ERROR;
+ } else {
+ statusString = MessageIds.TEST_FAILED;
+ }
+ TestReferenceFailure failure =
+ new TestReferenceFailure(new TestCaseReference(test),
+ statusString, trace, null);
+ mExecution.getListener().notifyTestFailed(failure);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
+ */
+ public void testRunEnded(long elapsedTime) {
+ notifyTestRunEnded(elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run complete");
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+ */
+ public void testRunFailed(String errorMessage) {
+ reportError(errorMessage);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+ */
+ public void testRunStarted(int testCount) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+ */
+ public void testRunStopped(long elapsedTime) {
+ notifyTestRunStopped(elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Test run stopped");
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ public void testStarted(TestIdentifier test) {
+ TestCaseReference testId = new TestCaseReference(test);
+ mExecution.getListener().notifyTestStarted(testId);
+ }
+ }
+} \ No newline at end of file
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java
new file mode 100644
index 000000000..1a0ee9a8b
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCaseReference.java
@@ -0,0 +1,75 @@
+/*
+ * 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.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+
+import java.text.MessageFormat;
+
+/**
+ * Reference for a single Android test method.
+ */
+@SuppressWarnings("restriction")
+class TestCaseReference extends AndroidTestReference {
+
+ private final String mClassName;
+ private final String mTestName;
+
+ /**
+ * Creates a TestCaseReference from a class and method name
+ */
+ TestCaseReference(String className, String testName) {
+ mClassName = className;
+ mTestName = testName;
+ }
+
+ /**
+ * Creates a TestCaseReference from a {@link TestIdentifier}
+ * @param test
+ */
+ TestCaseReference(TestIdentifier test) {
+ mClassName = test.getClassName();
+ mTestName = test.getTestName();
+ }
+
+ /**
+ * Returns a count of the number of test cases referenced. Is always one for this class.
+ */
+ public int countTestCases() {
+ return 1;
+ }
+
+ /**
+ * Sends test identifier and test count information for this test
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info to
+ */
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), false, countTestCases());
+ }
+
+ /**
+ * Returns the identifier of this test, in a format expected by JDT JUnit
+ */
+ public String getName() {
+ return MessageFormat.format(MessageIds.TEST_IDENTIFIER_MESSAGE_FORMAT,
+ new Object[] { mTestName, mClassName});
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java
new file mode 100644
index 000000000..2dc13a708
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestCollector.java
@@ -0,0 +1,124 @@
+/*
+ * 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.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collects info about tests to be executed by listening to the results of an Android test run.
+ */
+@SuppressWarnings("restriction")
+class TestCollector implements ITestRunListener {
+
+ private int mTotalTestCount;
+ /** test name to test suite reference map. */
+ private Map<String, TestSuiteReference> mTestTree;
+ private String mErrorMessage = null;
+
+ TestCollector() {
+ mTotalTestCount = 0;
+ mTestTree = new HashMap<String, TestSuiteReference>();
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testEnded(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ public void testEnded(TestIdentifier test) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.ITestRunListener.TestFailure, com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long)
+ */
+ public void testRunEnded(long elapsedTime) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+ */
+ public void testRunFailed(String errorMessage) {
+ mErrorMessage = errorMessage;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+ */
+ public void testRunStarted(int testCount) {
+ mTotalTestCount = testCount;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+ */
+ public void testRunStopped(long elapsedTime) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ public void testStarted(TestIdentifier test) {
+ TestSuiteReference suiteRef = mTestTree.get(test.getClassName());
+ if (suiteRef == null) {
+ // this test suite has not been seen before, create it
+ suiteRef = new TestSuiteReference(test.getClassName());
+ mTestTree.put(test.getClassName(), suiteRef);
+ }
+ suiteRef.addTest(new TestCaseReference(test));
+ }
+
+ /**
+ * Returns the total test count in the test run.
+ */
+ public int getTestCaseCount() {
+ return mTotalTestCount;
+ }
+
+ /**
+ * Sends info about the test tree to be executed (ie the suites and their enclosed tests)
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test data to
+ */
+ public void sendTrees(IVisitsTestTrees notified) {
+ for (ITestReference ref : mTestTree.values()) {
+ ref.sendTree(notified);
+ }
+ }
+
+ /**
+ * Returns the error message that was reported when collecting test info.
+ * Returns <code>null</code> if no error occurred.
+ */
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java
new file mode 100644
index 000000000..797f27b03
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/junit/runtime/TestSuiteReference.java
@@ -0,0 +1,78 @@
+/*
+ * 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.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Reference for an Android test suite aka class.
+ */
+@SuppressWarnings("restriction")
+class TestSuiteReference extends AndroidTestReference {
+
+ private final String mClassName;
+ private List<TestCaseReference> mTests;
+
+ /**
+ * Creates a TestSuiteReference
+ *
+ * @param className the fully qualified name of the test class
+ */
+ TestSuiteReference(String className) {
+ mClassName = className;
+ mTests = new ArrayList<TestCaseReference>();
+ }
+
+ /**
+ * Returns a count of the number of test cases included in this suite.
+ */
+ public int countTestCases() {
+ return mTests.size();
+ }
+
+ /**
+ * Sends test identifier and test count information for this test class, and all its included
+ * test methods.
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info too
+ */
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), true, countTestCases());
+ for (TestCaseReference ref : mTests) {
+ ref.sendTree(notified);
+ }
+ }
+
+ /**
+ * Return the name of this test class.
+ */
+ public String getName() {
+ return mClassName;
+ }
+
+ /**
+ * Adds a test method to this suite.
+ *
+ * @param testRef the {@link TestCaseReference} to add
+ */
+ void addTest(TestCaseReference testRef) {
+ mTests.add(testRef);
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java
index c650b9846..e091b1385 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java
+++ b/tools/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<IJavaProject> getAndroidProjectDependencies(IJavaProject javaProject)
+ throws JavaModelException {
+ String[] requiredProjectNames = javaProject.getRequiredProjectNames();
+
+ // Go from java project name to JavaProject name
+ IJavaModel javaModel = javaProject.getJavaModel();
+
+ // loop through all dependent projects and keep only those that are Android projects
+ List<IJavaProject> projectList = new ArrayList<IJavaProject>(requiredProjectNames.length);
+ for (String javaProjectName : requiredProjectNames) {
+ IJavaProject androidJavaProject = javaModel.getJavaProject(javaProjectName);
+
+ //Verify that the project has also the Android Nature
+ try {
+ if (!androidJavaProject.getProject().hasNature(AndroidConstants.NATURE)) {
+ continue;
+ }
+ } catch (CoreException e) {
+ continue;
+ }
+
+ projectList.add(androidJavaProject);
+ }
+
+ return projectList;
+ }
+
+ /**
+ * Returns the android package file as an IFile object for the specified
+ * project.
+ * @param project The project
+ * @return The android package as an IFile object or null if not found.
+ */
+ public static IFile getApplicationPackage(IProject project) {
+ // get the output folder
+ IFolder outputLocation = BaseProjectHelper.getOutputFolder(project);
+
+ if (outputLocation == null) {
+ AdtPlugin.printErrorToConsole(project,
+ "Failed to get the output location of the project. Check build path properties"
+ );
+ return null;
+ }
+
+
+ // get the package path
+ String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
+ IResource r = outputLocation.findMember(packageName);
+
+ // check the package is present
+ if (r instanceof IFile && r.exists()) {
+ return (IFile)r;
+ }
+
+ String msg = String.format("Could not find %1$s!", packageName);
+ AdtPlugin.printErrorToConsole(project, msg);
+
+ return null;
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
index 5aeb3358c..e9df77f65 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
@@ -487,6 +487,15 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit
IJavaProject javaProject = projects.get(i);
IProject iProject = javaProject.getProject();
+ // check if the project is opened
+ if (iProject.isOpen() == false) {
+ // remove from the list
+ // we do not increment i in this case.
+ projects.remove(i);
+
+ continue;
+ }
+
// get the target from the project and its paths
IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
if (target == null) {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java
new file mode 100644
index 000000000..4ef1268de
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringAction.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.refactorings.extractstring;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.FileEditorInput;
+
+/*
+ * Quick Reference Link:
+ * http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html
+ * and
+ * http://www.ibm.com/developerworks/opensource/library/os-ecjdt/
+ */
+
+/**
+ * Action executed when the "Extract String" menu item is invoked.
+ * <p/>
+ * The intent of the action is to start a refactoring that extracts a source string and
+ * replaces it by an Android string resource ID.
+ * <p/>
+ * Workflow:
+ * <ul>
+ * <li> The action is currently located in the Refactoring menu in the main menu.
+ * <li> TODO: extend the popup refactoring menu in a Java or Android XML file.
+ * <li> The action is only enabled if the selection is 1 character or more. That is at least part
+ * of the string must be selected, it's not enough to just move the insertion point. This is
+ * a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when
+ * the insertion point is merely moved. TODO: address this limitation.
+ * <ul> The action gets the current {@link ISelection}. It also knows the current
+ * {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the
+ * actual resource file. By looking at the Active Window > Active Page > Active Editor we
+ * can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file)
+ * that is being edited.
+ * <ul> TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit}
+ * can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow
+ * us to be able to work with a selection from an Android XML file later.
+ * <li> The action creates a new {@link ExtractStringRefactoring} and make it run on in a new
+ * {@link ExtractStringWizard}.
+ * <ul>
+ */
+public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
+
+ /** Keep track of the current workbench window. */
+ private IWorkbenchWindow mWindow;
+ private ITextSelection mSelection;
+ private IFile mFile;
+
+ /**
+ * Keep track of the current workbench window.
+ */
+ public void init(IWorkbenchWindow window) {
+ mWindow = window;
+ }
+
+ public void dispose() {
+ // Nothing to do
+ }
+
+ /**
+ * Examine the selection to determine if the action should be enabled or not.
+ * <p/>
+ * Keep a link to the relevant selection structure (i.e. a part of the Java AST).
+ */
+ public void selectionChanged(IAction action, ISelection selection) {
+
+ // Note, two kinds of selections are returned here:
+ // ITextSelection on a Java source window
+ // IStructuredSelection in the outline or navigator
+ // This simply deals with the refactoring based on a non-empty selection.
+ // At that point, just enable the action and later decide if it's valid when it actually
+ // runs since we don't have access to the AST yet.
+
+ mSelection = null;
+ mFile = null;
+
+ if (selection instanceof ITextSelection) {
+ mSelection = (ITextSelection) selection;
+ if (mSelection.getLength() > 0) {
+ mFile = getSelectedFile();
+ }
+ }
+
+ action.setEnabled(mSelection != null && mFile != null);
+ }
+
+ /**
+ * Create a new instance of our refactoring and a wizard to configure it.
+ */
+ public void run(IAction action) {
+ if (mSelection != null && mFile != null) {
+ ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mSelection);
+ RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject());
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ /**
+ * Returns the active {@link IFile} (hopefully matching our selection) or null.
+ * The file is only returned if it's a file from a project with an Android nature.
+ * <p/>
+ * At that point we do not try to analyze if the selection nor the file is suitable
+ * for the refactoring. This check is performed when the refactoring is invoked since
+ * it can then produce meaningful error messages as needed.
+ */
+ private IFile getSelectedFile() {
+ IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (wwin != null) {
+ IWorkbenchPage page = wwin.getActivePage();
+ if (page != null) {
+ IEditorPart editor = page.getActiveEditor();
+ if (editor != null) {
+ IEditorInput input = editor.getEditorInput();
+
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fi = (FileEditorInput) input;
+ IFile file = fi.getFile();
+ if (file.exists()) {
+ IProject proj = file.getProject();
+ try {
+ if (proj != null && proj.hasNature(AndroidConstants.NATURE)) {
+ return file;
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringContribution.java b/tools/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/tools/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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringDescriptor.java b/tools/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/tools/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<String, String> mArguments;
+
+ public ExtractStringDescriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super(ID, project, description, comment,
+ RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags
+ );
+ mArguments = arguments;
+ }
+
+ public Map<String, String> getArguments() {
+ return mArguments;
+ }
+
+ /**
+ * Creates a new refactoring instance for this refactoring descriptor based on
+ * an argument map. The argument map is created by the refactoring itself in
+ * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)}
+ * <p/>
+ * This is apparently used to replay a refactoring.
+ *
+ * {@inheritDoc}
+ *
+ * @throws CoreException
+ */
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ try {
+ ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments);
+ return ref;
+ } catch (NullPointerException e) {
+ status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor");
+ return null;
+ }
+ }
+
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java
new file mode 100644
index 000000000..5ffeeb05f
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java
@@ -0,0 +1,477 @@
+/*
+ * 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.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.HashMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @see ExtractStringRefactoring
+ */
+class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
+
+ /** Last res file path used, shared across the session instances but specific to the
+ * current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */
+ private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>();
+
+ /** The project where the user selection happened. */
+ private final IProject mProject;
+
+ /** Field displaying the user-selected string to be replaced. */
+ private Label mStringLabel;
+ /** Test field where the user enters the new ID to be generated or replaced with. */
+ private Text mNewIdTextField;
+ /** The configuration selector, to select the resource path of the XML file. */
+ private ConfigurationSelector mConfigSelector;
+ /** The combo to display the existing XML files or enter a new one. */
+ private Combo mResFileCombo;
+
+ /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
+ * a leaf file name ending with .xml */
+ private static final Pattern RES_XML_FILE_REGEX = Pattern.compile(
+ "/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$
+ /** Absolute destination folder root, e.g. "/res/" */
+ private static final String RES_FOLDER_ABS =
+ AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
+ /** Relative destination folder root, e.g. "res/" */
+ private static final String RES_FOLDER_REL =
+ SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
+
+ private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml";
+
+ public ExtractStringInputPage(IProject project) {
+ super("ExtractStringInputPage"); //$NON-NLS-1$
+ mProject = project;
+ }
+
+ /**
+ * Create the UI for the refactoring wizard.
+ * <p/>
+ * Note that at that point the initial conditions have been checked in
+ * {@link ExtractStringRefactoring}.
+ */
+ public void createControl(Composite parent) {
+
+ Composite content = new Composite(parent, SWT.NONE);
+
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 1;
+ content.setLayout(layout);
+
+ createStringReplacementGroup(content);
+ createResFileGroup(content);
+
+ validatePage();
+ setControl(content);
+ }
+
+ /**
+ * Creates the top group with the field to replace which string and by what
+ * and by which options.
+ *
+ * @param content A composite with a 1-column grid layout
+ */
+ private void createStringReplacementGroup(Composite content) {
+
+ final ExtractStringRefactoring ref = getOurRefactoring();
+
+ Group group = new Group(content, SWT.NONE);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ group.setText("String Replacement");
+
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+
+ // line: String found in selection
+
+ Label label = new Label(group, SWT.NONE);
+ label.setText("String:");
+
+ String selectedString = ref.getTokenString();
+
+ mStringLabel = new Label(group, 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 : Textfield for new ID
+
+ label = new Label(group, SWT.NONE);
+ label.setText("Replace by R.string.");
+
+ mNewIdTextField = new Text(group, 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.setReplacementStringId(mNewIdTextField.getText().trim());
+ }
+ }
+ });
+ }
+
+ /**
+ * Creates the lower group with the fields to choose the resource confirmation and
+ * the target XML file.
+ *
+ * @param content A composite with a 1-column grid layout
+ */
+ private void createResFileGroup(Composite content) {
+
+ Group group = new Group(content, SWT.NONE);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ group.setText("XML resource to edit");
+
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+
+ // line: selection of the res config
+
+ Label label;
+ label = new Label(group, SWT.NONE);
+ label.setText("Configuration:");
+
+ mConfigSelector = new ConfigurationSelector(group);
+ GridData gd = new GridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mConfigSelector.setLayoutData(gd);
+ OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
+ mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
+
+ // line: selection of the output file
+
+ label = new Label(group, SWT.NONE);
+ label.setText("Resource file:");
+
+ mResFileCombo = new Combo(group, SWT.DROP_DOWN);
+ mResFileCombo.select(0);
+ mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mResFileCombo.addModifyListener(onConfigSelectorUpdated);
+
+ // set output file name to the last one used
+
+ String projPath = mProject.getFullPath().toPortableString();
+ String filePath = sLastResFilePath.get(projPath);
+
+ mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
+ onConfigSelectorUpdated.run();
+ }
+
+ /**
+ * Utility method to guess a suitable new XML ID based on the selected string.
+ */
+ private String guessId(String text) {
+ // make lower case
+ text = text.toLowerCase();
+
+ // everything not alphanumeric becomes an underscore
+ text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // the id must be a proper Java identifier, so it can't start with a number
+ if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) {
+ text = "_" + text; //$NON-NLS-1$
+ }
+ return text;
+ }
+
+ /**
+ * Returns the {@link ExtractStringRefactoring} instance used by this wizard page.
+ */
+ private ExtractStringRefactoring getOurRefactoring() {
+ return (ExtractStringRefactoring) getRefactoring();
+ }
+
+ /**
+ * Validates fields of the wizard input page. Displays errors as appropriate and
+ * enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}.
+ *
+ * @return True if the page has been positively validated. It may still have warnings.
+ */
+ private boolean validatePage() {
+ boolean success = true;
+
+ // Analyze fatal errors.
+
+ String text = mNewIdTextField.getText().trim();
+ 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;
+ }
+ }
+ }
+
+ String resFile = mResFileCombo.getText();
+ if (success) {
+ if (resFile == null || resFile.length() == 0) {
+ setErrorMessage("A resource file name is required.");
+ success = false;
+ } else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) {
+ setErrorMessage("The XML file name is not valid.");
+ success = false;
+ }
+ }
+
+ // Analyze info & warnings.
+
+ if (success) {
+ setErrorMessage(null);
+
+ ExtractStringRefactoring ref = getOurRefactoring();
+
+ ref.setTargetFile(resFile);
+ sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
+
+ if (ref.isResIdDuplicate(resFile, text)) {
+ setMessage(
+ String.format("There's already a string item called '%1$s' in %2$s.",
+ text, resFile),
+ WizardPage.WARNING);
+ } else if (mProject.findMember(resFile) == null) {
+ setMessage(
+ String.format("File %2$s does not exist and will be created.",
+ text, resFile),
+ WizardPage.INFORMATION);
+ } else {
+ setMessage(null);
+ }
+ }
+
+ setPageComplete(success);
+ return success;
+ }
+
+ public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
+
+ /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
+ private final Pattern mPathRegex = Pattern.compile(
+ "(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$
+
+ /** Temporary config object used to retrieve the Config Selector value. */
+ private FolderConfiguration mTempConfig = new FolderConfiguration();
+
+ private HashMap<String, TreeSet<String>> mFolderCache =
+ new HashMap<String, TreeSet<String>>();
+ private String mLastFolderUsedInCombo = null;
+ private boolean mInternalConfigChange;
+ private boolean mInternalFileComboChange;
+
+ /**
+ * Callback invoked when the {@link ConfigurationSelector} has been changed.
+ * <p/>
+ * The callback does the following:
+ * <ul>
+ * <li> Examine the current file name to retrieve the XML filename, if any.
+ * <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/).
+ * <li> Examine the path to retrieve all the files in it. Keep those in a local cache.
+ * <li> If the XML filename from step 1 is not in the file list, it's a custom file name.
+ * Insert it and sort it.
+ * <li> Re-populate the file combo with all the choices.
+ * <li> Select the original XML file.
+ */
+ public void run() {
+ if (mInternalConfigChange) {
+ return;
+ }
+
+ // get current leafname, if any
+ String leafName = "";
+ String currPath = mResFileCombo.getText();
+ Matcher m = mPathRegex.matcher(currPath);
+ if (m.matches()) {
+ // Note: groups 1 and 2 cannot be null.
+ leafName = m.group(2);
+ currPath = m.group(1);
+ } else {
+ // There was a path but it was invalid. Ignore it.
+ currPath = "";
+ }
+
+ // recreate the res path from the current configuration
+ mConfigSelector.getConfiguration(mTempConfig);
+ StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
+ sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
+ sb.append('/');
+
+ String newPath = sb.toString();
+ if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
+ // Path has not changed. No need to reload.
+ return;
+ }
+
+ // Get all the files at the new path
+
+ TreeSet<String> filePaths = mFolderCache.get(newPath);
+
+ if (filePaths == null) {
+ filePaths = new TreeSet<String>();
+
+ IFolder folder = mProject.getFolder(newPath);
+ if (folder != null && folder.exists()) {
+ try {
+ for (IResource res : folder.members()) {
+ String name = res.getName();
+ if (res.getType() == IResource.FILE && name.endsWith(".xml")) {
+ filePaths.add(newPath + name);
+ }
+ }
+ } catch (CoreException e) {
+ // Ignore.
+ }
+ }
+
+ mFolderCache.put(newPath, filePaths);
+ }
+
+ currPath = newPath + leafName;
+ if (leafName.length() > 0 && !filePaths.contains(currPath)) {
+ filePaths.add(currPath);
+ }
+
+ // Fill the combo
+ try {
+ mInternalFileComboChange = true;
+
+ mResFileCombo.removeAll();
+
+ for (String filePath : filePaths) {
+ mResFileCombo.add(filePath);
+ }
+
+ int index = -1;
+ if (leafName.length() > 0) {
+ index = mResFileCombo.indexOf(currPath);
+ if (index >= 0) {
+ mResFileCombo.select(index);
+ }
+ }
+
+ if (index == -1) {
+ mResFileCombo.setText(currPath);
+ }
+
+ mLastFolderUsedInCombo = newPath;
+
+ } finally {
+ mInternalFileComboChange = false;
+ }
+
+ // finally validate the whole page
+ validatePage();
+ }
+
+ /**
+ * Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been
+ * modified.
+ */
+ public void modifyText(ModifyEvent e) {
+ if (mInternalFileComboChange) {
+ return;
+ }
+
+ String wsFolderPath = mResFileCombo.getText();
+
+ // This is a custom path, we need to sanitize it.
+ // First it should start with "/res/". Then we need to make sure there are no
+ // relative paths, things like "../" or "./" or even "//".
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
+ if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
+ wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
+
+ mInternalFileComboChange = true;
+ mResFileCombo.setText(wsFolderPath);
+ mInternalFileComboChange = false;
+ }
+
+ if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
+
+ int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
+ if (pos >= 0) {
+ wsFolderPath = wsFolderPath.substring(0, pos);
+ }
+
+ String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
+
+ if (folderSegments.length > 0) {
+ String folderName = folderSegments[0];
+
+ if (folderName != null && !folderName.equals(wsFolderPath)) {
+ // update config selector
+ mInternalConfigChange = true;
+ mConfigSelector.setConfiguration(folderSegments);
+ mInternalConfigChange = false;
+ }
+ }
+ }
+
+ validatePage();
+ }
+ }
+
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java
new file mode 100644
index 000000000..430ff1819
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java
@@ -0,0 +1,965 @@
+/*
+ * 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.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jdt.core.IBuffer;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.ToolFactory;
+import org.eclipse.jdt.core.compiler.IScanner;
+import org.eclipse.jdt.core.compiler.ITerminalSymbols;
+import org.eclipse.jdt.core.compiler.InvalidInputException;
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.Name;
+import org.eclipse.jdt.core.dom.QualifiedName;
+import org.eclipse.jdt.core.dom.SimpleName;
+import org.eclipse.jdt.core.dom.StringLiteral;
+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
+import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.text.edits.TextEditGroup;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * This refactoring extracts a string from a file and replaces it by an Android resource ID
+ * such as R.string.foo.
+ * <p/>
+ * There are a number of scenarios, which are not all supported yet. The workflow works as
+ * such:
+ * <ul>
+ * <li> User selects a string in a Java (TODO: or XML file) and invokes
+ * the {@link ExtractStringAction}.
+ * <li> The action finds the {@link ICompilationUnit} being edited as well as the current
+ * {@link ITextSelection}. The action creates a new instance of this refactoring as
+ * well as an {@link ExtractStringWizard} and runs the operation.
+ * <li> TODO: to support refactoring from an XML file, the action should give the {@link IFile}
+ * and then here we would have to determine whether it's a suitable Android XML file or a
+ * suitable Java file.
+ * TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout
+ * files or text elements (e.g. <string>foo</string>) for values, etc.
+ * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
+ * that the java source is not read-only and is in sync. We also try to find a string under
+ * the selection. If this fails, the refactoring is aborted.
+ * <li> TODO: Find the string in an XML file based on selection.
+ * <li> On success, the wizard is shown, which let the user input the new ID to use.
+ * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
+ * ID, the XML file to update, etc. The wizard does use the utility method
+ * {@link #isResIdDuplicate(String, String)} to check whether the new ID is already defined
+ * in the target XML file.
+ * <li> Once Preview or Finish is selected in the wizard, the
+ * {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
+ * and compute the actual changes.
+ * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
+ * </ul>
+ *
+ * The list of changes are:
+ * <ul>
+ * <li> If the target XML does not exist, create it with the new string ID.
+ * <li> If the target XML exists, find the <resources> node and add the new string ID right after.
+ * If the node is <resources/>, it needs to be opened.
+ * <li> Create an AST rewriter to edit the source Java file and replace all occurences by the
+ * new computed R.string.foo. Also need to rewrite imports to import R as needed.
+ * If there's already a conflicting R included, we need to insert the FQCN instead.
+ * <li> TODO: If the source is an XML file, determine if we need to change an attribute or a
+ * a text element.
+ * <li> TODO: Have a pref in the wizard: [x] Change other XML Files
+ * <li> TODO: Have a pref in the wizard: [x] Change other Java Files
+ * </ul>
+ */
+class ExtractStringRefactoring extends Refactoring {
+
+ /** The file model being manipulated. */
+ private final IFile mFile;
+ /** The start of the selection in {@link #mFile}. */
+ private final int mSelectionStart;
+ /** The end of the selection in {@link #mFile}. */
+ private final int mSelectionEnd;
+
+ /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
+ private ICompilationUnit mUnit;
+ /** The actual string selected, after UTF characters have been escaped, good for display. */
+ private String mTokenString;
+
+ /** The XML string ID selected by the user in the wizard. */
+ private String mXmlStringId;
+ /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
+ * in the wizard. */
+ private String mTargetXmlFileWsPath;
+
+ /** A temporary cache of R.string IDs defined by a given xml file. The key is the
+ * project path of the file, the data is a set of known string Ids for that file. */
+ private HashMap<String,HashSet<String>> mResIdCache;
+ /** An instance of XPath, created lazily on demand. */
+ private XPath mXPath;
+ /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
+ * used by {@link #createChange(IProgressMonitor)}. */
+ private ArrayList<Change> mChanges;
+
+ public ExtractStringRefactoring(Map<String, String> arguments)
+ throws NullPointerException {
+
+ IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
+ mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
+ mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
+ mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
+ }
+
+ private Map<String, String> createArgumentMap() {
+ HashMap<String, String> args = new HashMap<String, String>();
+ args.put("file", mFile.getFullPath().toPortableString()); //$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-esc", mTokenString); //$NON-NLS-1$
+ return args;
+ }
+
+ public ExtractStringRefactoring(IFile file, ITextSelection selection) {
+ mFile = file;
+ mSelectionStart = selection.getOffset();
+ mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
+ }
+
+ /**
+ * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
+ */
+ @Override
+ public String getName() {
+ return "Extract Android String";
+ }
+
+ /**
+ * Gets the actual string selected, after UTF characters have been escaped,
+ * good for display.
+ */
+ public String getTokenString() {
+ return mTokenString;
+ }
+
+ /**
+ * Step 1 of 3 of the refactoring:
+ * Checks that the current selection meets the initial condition before the ExtractString
+ * wizard is shown. The check is supposed to be lightweight and quick. Note that at that
+ * point the wizard has not been created yet.
+ * <p/>
+ * Here we scan the source buffer to find the token matching the selection.
+ * The check is successful is a Java string literal is selected, the source is in sync
+ * and is not read-only.
+ * <p/>
+ * This is also used to extract the string to be modified, so that we can display it in
+ * the refactoring wizard.
+ *
+ * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
+ *
+ * @throws CoreException
+ */
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
+ throws CoreException, OperationCanceledException {
+
+ mUnit = null;
+ mTokenString = null;
+
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ monitor.beginTask("Checking preconditions...", 5);
+
+ if (!checkSourceFile(mFile, status, monitor)) {
+ return status;
+ }
+
+ // Try to get a compilation unit from this file. If it fails, mUnit is null.
+ try {
+ mUnit = JavaCore.createCompilationUnitFrom(mFile);
+
+ // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar
+ if (mUnit.isReadOnly()) {
+ status.addFatalError("The file is read-only, please make it writeable first.");
+ return status;
+ }
+
+ // This is a Java file. Check if it contains the selection we want.
+ if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
+ return status;
+ }
+
+ } catch (Exception e) {
+ // That was not a Java file. Ignore.
+ }
+
+ if (mUnit == null) {
+ // Check this an XML file and get the selection and its context.
+ // TODO
+ status.addFatalError("Selection must be inside a Java source file.");
+ }
+ } finally {
+ monitor.done();
+ }
+
+ return status;
+ }
+
+ /**
+ * Try to find the selected Java element in the compilation unit.
+ *
+ * If selection matches a string literal, capture it, otherwise add a fatal error
+ * to the status.
+ *
+ * On success, advance the monitor by 3.
+ */
+ private boolean findSelectionInJavaUnit(ICompilationUnit unit,
+ RefactoringStatus status, IProgressMonitor monitor) {
+ try {
+ IBuffer buffer = unit.getBuffer();
+
+ IScanner scanner = ToolFactory.createScanner(
+ false, //tokenizeComments
+ false, //tokenizeWhiteSpace
+ false, //assertMode
+ false //recordLineSeparator
+ );
+ scanner.setSource(buffer.getCharacters());
+ monitor.worked(1);
+
+ for(int token = scanner.getNextToken();
+ token != ITerminalSymbols.TokenNameEOF;
+ token = scanner.getNextToken()) {
+ if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
+ scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
+ // found the token, but only keep of the right type
+ if (token == ITerminalSymbols.TokenNameStringLiteral) {
+ mTokenString = new String(scanner.getCurrentTokenSource());
+ }
+ break;
+ } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) {
+ // scanner is past the selection, abort.
+ break;
+ }
+ }
+ } catch (JavaModelException e1) {
+ // Error in unit.getBuffer. Ignore.
+ } catch (InvalidInputException e2) {
+ // Error in scanner.getNextToken. Ignore.
+ } finally {
+ monitor.worked(1);
+ }
+
+ if (mTokenString != null) {
+ // As a literal string, the token should have surrounding quotes. Remove them.
+ int len = mTokenString.length();
+ if (len > 0 &&
+ mTokenString.charAt(0) == '"' &&
+ mTokenString.charAt(len - 1) == '"') {
+ mTokenString = mTokenString.substring(1, len - 1);
+ }
+ // We need a non-empty string literal
+ if (mTokenString.length() == 0) {
+ mTokenString = null;
+ }
+ }
+
+ if (mTokenString == null) {
+ status.addFatalError("Please select a Java string literal.");
+ }
+
+ monitor.worked(1);
+ return status.isOK();
+ }
+
+ /**
+ * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
+ * Might not be useful.
+ *
+ * On success, advance the monitor by 2.
+ *
+ * @return False if caller should abort, true if caller should continue.
+ */
+ private boolean checkSourceFile(IFile file,
+ RefactoringStatus status,
+ IProgressMonitor monitor) {
+ // check whether the source file is in sync
+ if (!file.isSynchronized(IResource.DEPTH_ZERO)) {
+ status.addFatalError("The file is not synchronized. Please save it first.");
+ return false;
+ }
+ monitor.worked(1);
+
+ // make sure we can write to it.
+ ResourceAttributes resAttr = file.getResourceAttributes();
+ if (resAttr == null || resAttr.isReadOnly()) {
+ status.addFatalError("The file is read-only, please make it writeable first.");
+ return false;
+ }
+ monitor.worked(1);
+
+ return true;
+ }
+
+ /**
+ * Step 2 of 3 of the refactoring:
+ * Check the conditions once the user filled values in the refactoring wizard,
+ * then prepare the changes to be applied.
+ * <p/>
+ * In this case, most of the sanity checks are done by the wizard so essentially this
+ * should only be called if the wizard positively validated the user input.
+ *
+ * Here we do check that the target resource XML file either does not exists or
+ * is not read-only.
+ *
+ * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
+ *
+ * @throws CoreException
+ */
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
+ throws CoreException, OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ monitor.beginTask("Checking post-conditions...", 3);
+
+ if (mXmlStringId == null || mXmlStringId.length() <= 0) {
+ // this is not supposed to happen
+ status.addFatalError("Missing replacement string ID");
+ } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) {
+ // this is not supposed to happen
+ status.addFatalError("Missing target xml file path");
+ }
+ monitor.worked(1);
+
+ // Either that resource must not exist or it must be a writeable file.
+ IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
+ if (targetXml != null) {
+ if (targetXml.getType() != IResource.FILE) {
+ status.addFatalError(
+ String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath));
+ } else {
+ ResourceAttributes attr = targetXml.getResourceAttributes();
+ if (attr != null && attr.isReadOnly()) {
+ status.addFatalError(
+ String.format("XML file '%1$s' is read-only.",
+ mTargetXmlFileWsPath));
+ }
+ }
+ }
+ monitor.worked(1);
+
+ if (status.hasError()) {
+ return status;
+ }
+
+ mChanges = new ArrayList<Change>();
+
+
+ // Prepare the change for the XML file.
+
+ if (!isResIdDuplicate(mTargetXmlFileWsPath, mXmlStringId)) {
+ // We actually change it only if the ID doesn't exist yet
+ Change change = createXmlChange((IFile) targetXml, mXmlStringId, mTokenString,
+ status, SubMonitor.convert(monitor, 1));
+ if (change != null) {
+ mChanges.add(change);
+ }
+ }
+
+ if (status.hasError()) {
+ return status;
+ }
+
+ // Prepare the change to the Java compilation unit
+ List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
+ status, SubMonitor.convert(monitor, 1));
+ if (changes != null) {
+ mChanges.addAll(changes);
+ }
+
+ monitor.worked(1);
+ } finally {
+ monitor.done();
+ }
+
+ return status;
+ }
+
+ /**
+ * Internal helper that actually prepares the {@link Change} that adds the given
+ * ID to the given XML File.
+ * <p/>
+ * This does not actually modify the file.
+ *
+ * @param targetXml The file resource to modify.
+ * @param xmlStringId The new ID to insert.
+ * @param tokenString The old string, which will be the value in the XML string.
+ * @return A new {@link TextEdit} that describes how to change the file.
+ */
+ private Change createXmlChange(IFile targetXml,
+ String xmlStringId,
+ String tokenString,
+ RefactoringStatus status,
+ SubMonitor subMonitor) {
+
+ TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
+ xmlChange.setTextType("xml"); //$NON-NLS-1$
+
+ TextEdit edit = null;
+ TextEditGroup editGroup = null;
+
+ if (!targetXml.exists()) {
+ // The XML file does not exist. Simply create it.
+ StringBuilder content = new StringBuilder();
+ content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+ content.append("<resources>\n"); //$NON-NLS-1$
+ content.append(" <string name=\""). //$NON-NLS-1$
+ append(xmlStringId).
+ append("\">"). //$NON-NLS-1$
+ append(tokenString).
+ append("</string>\n"); //$NON-NLS-1$
+ content.append("<resources>\n"); //$NON-NLS-1$
+
+ edit = new InsertEdit(0, content.toString());
+ editGroup = new TextEditGroup("Create ID in new XML file", edit);
+ } else {
+ // The file exist. Attempt to parse it as a valid XML document.
+ try {
+ int[] indices = new int[2];
+ if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$
+ // Indices[1] indicates whether we found > or />. It can only be 1 or 2.
+ // Indices[0] is the position of the first character of either > or />.
+ //
+ // Note: we don't even try to adapt our formatting to the existing structure (we
+ // could by capturing whatever whitespace is after the closing bracket and
+ // applying it here before our tag, unless we were dealing with an empty
+ // resource tag.)
+
+ int offset = indices[0];
+ int len = indices[1];
+ StringBuilder content = new StringBuilder();
+ content.append(">\n"); //$NON-NLS-1$
+ content.append(" <string name=\""). //$NON-NLS-1$
+ append(xmlStringId).
+ append("\">"). //$NON-NLS-1$
+ append(tokenString).
+ append("</string>"); //$NON-NLS-1$
+ if (len == 2) {
+ content.append("\n</resources>"); //$NON-NLS-1$
+ }
+
+ edit = new ReplaceEdit(offset, len, content.toString());
+ editGroup = new TextEditGroup("Insert ID in XML file", edit);
+ }
+ } catch (CoreException e) {
+ // Failed to read file. Ignore. Will return null below.
+ }
+ }
+
+ if (edit == null) {
+ status.addFatalError(String.format("Failed to modify file %1$s",
+ mTargetXmlFileWsPath));
+ return null;
+ }
+
+ xmlChange.setEdit(edit);
+ // The TextEditChangeGroup let the user toggle this change on and off later.
+ xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup));
+
+ subMonitor.worked(1);
+ return xmlChange;
+ }
+
+ /**
+ * Parse an XML input stream, looking for an opening tag.
+ * <p/>
+ * If found, returns the character offest in the buffer of the closing bracket of that
+ * tag, e.g. the position of > in "<resources>". The first character is at offset 0.
+ * <p/>
+ * The implementation here relies on a simple character-based parser. No DOM nor SAX
+ * parsing is used, due to the simplified nature of the task: we just want the first
+ * opening tag, which in our case should be the document root. We deal however with
+ * with the tag being commented out, so comments are skipped. We assume the XML doc
+ * is sane, e.g. we don't expect the tag to appear in the middle of a string. But
+ * again since in fact we want the root element, that's unlikely to happen.
+ * <p/>
+ * We need to deal with the case where the element is written as <resources/>, in
+ * which case the caller will want to replace /> by ">...</...>". To do that we return
+ * two values: the first offset of the closing tag (e.g. / or >) and the length, which
+ * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >.
+ *
+ * @param contents An existing buffer to parse.
+ * @param tag The tag to look for.
+ * @param indices The return values: [0] is the offset of the closing bracket and [1] is
+ * the length which can be only 1 for > and 2 for />
+ * @return True if we found the tag, in which case <code>indices</code> can be used.
+ */
+ private boolean findXmlOpeningTagPos(InputStream contents, String tag, int[] indices) {
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(contents));
+ StringBuilder sb = new StringBuilder(); // scratch area
+
+ tag = "<" + tag;
+ int tagLen = tag.length();
+ int maxLen = tagLen < 3 ? 3 : tagLen;
+
+ try {
+ int offset = 0;
+ int i = 0;
+ char searching = '<'; // we want opening tags
+ boolean capture = false;
+ boolean inComment = false;
+ boolean inTag = false;
+ while ((i = br.read()) != -1) {
+ char c = (char) i;
+ if (c == searching) {
+ capture = true;
+ }
+ if (capture) {
+ sb.append(c);
+ int len = sb.length();
+ if (inComment && c == '>') {
+ // is the comment being closed?
+ if (len >= 3 && sb.substring(len-3).equals("-->")) { //$NON-NLS-1$
+ // yes, comment is closing, stop capturing
+ capture = false;
+ inComment = false;
+ sb.setLength(0);
+ }
+ } else if (inTag && c == '>') {
+ // we're capturing in our tag, waiting for the closing >, we just got it
+ // so we're totally done here. Simply detect whether it's /> or >.
+ indices[0] = offset;
+ indices[1] = 1;
+ if (sb.charAt(len - 2) == '/') {
+ indices[0]--;
+ indices[1]++;
+ }
+ return true;
+
+ } else if (!inComment && !inTag) {
+ // not a comment and not our tag yet, so we're capturing because a
+ // tag is being opened but we don't know which one yet.
+
+ // look for either the opening or a comment or
+ // the opening of our tag.
+ if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$
+ inComment = true;
+ } else if (len == tagLen && sb.toString().equals(tag)) {
+ inTag = true;
+ }
+
+ // if we're not interested in this tag yet, deal with when to stop
+ // capturing: the opening tag ends with either any kind of whitespace
+ // or with a > or maybe there's a PI that starts with <?
+ if (!inComment && !inTag) {
+ if (c == '>' || c == '?' || c == ' ' || c == '\n' || c == '\r') {
+ // stop capturing
+ capture = false;
+ sb.setLength(0);
+ }
+ }
+ }
+
+ if (capture && len > maxLen) {
+ // in any case we don't need to capture more than the size of our tag
+ // or the comment opening tag
+ sb.deleteCharAt(0);
+ }
+ }
+ offset++;
+ }
+ } catch (IOException e) {
+ // Ignore.
+ } finally {
+ try {
+ br.close();
+ } catch (IOException e) {
+ // oh come on...
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
+ */
+ private List<Change> computeJavaChanges(ICompilationUnit unit,
+ String xmlStringId,
+ String tokenString,
+ RefactoringStatus status,
+ SubMonitor subMonitor) {
+
+ // Get the Android package name from the Android Manifest. We need it to create
+ // the FQCN of the R class.
+ String packageName = null;
+ String error = null;
+ IProject proj = unit.getJavaProject().getProject();
+ IResource manifestFile = proj.findMember(AndroidConstants.FN_ANDROID_MANIFEST);
+ if (manifestFile == null || manifestFile.getType() != IResource.FILE) {
+ error = "File not found";
+ } else {
+ try {
+ AndroidManifestParser manifest = AndroidManifestParser.parseForData(
+ (IFile) manifestFile);
+ if (manifest == null) {
+ error = "Invalid content";
+ } else {
+ packageName = manifest.getPackage();
+ if (packageName == null) {
+ error = "Missing package definition";
+ }
+ }
+ } catch (CoreException e) {
+ error = e.getLocalizedMessage();
+ }
+ }
+
+ if (error != null) {
+ status.addFatalError(
+ String.format("Failed to parse file %1$s: %2$s.",
+ manifestFile.getFullPath(), error));
+ return null;
+ }
+
+ // TODO in a future version we might want to collect various Java files that
+ // need to be updated in the same project and process them all together.
+ // To do that we need to use an ASTRequestor and parser.createASTs, kind of
+ // like this:
+ //
+ // ASTRequestor requestor = new ASTRequestor() {
+ // @Override
+ // public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) {
+ // super.acceptAST(sourceUnit, astNode);
+ // // TODO process astNode
+ // }
+ // };
+ // ...
+ // parser.createASTs(compilationUnits, bindingKeys, requestor, monitor)
+ //
+ // and then add multiple TextFileChange to the changes arraylist.
+
+ // Right now the changes array will contain one TextFileChange at most.
+ ArrayList<Change> changes = new ArrayList<Change>();
+
+ // This is the unit that will be modified.
+ TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource());
+ change.setTextType("java"); //$NON-NLS-1$
+
+ // Create an AST for this compilation unit
+ ASTParser parser = ASTParser.newParser(AST.JLS3);
+ parser.setProject(unit.getJavaProject());
+ parser.setSource(unit);
+ parser.setResolveBindings(true);
+ ASTNode node = parser.createAST(subMonitor.newChild(1));
+
+ // The ASTNode must be a CompilationUnit, by design
+ if (!(node instanceof CompilationUnit)) {
+ status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$
+ node.getClass()));
+ return null;
+ }
+
+ // ImportRewrite will allow us to add the new type to the imports and will resolve
+ // what the Java source must reference, e.g. the FQCN or just the simple name.
+ ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true);
+ String Rqualifier = packageName + ".R"; //$NON-NLS-1$
+ Rqualifier = importRewrite.addImport(Rqualifier);
+
+ // Rewrite the AST itself via an ASTVisitor
+ AST ast = node.getAST();
+ ASTRewrite astRewrite = ASTRewrite.create(ast);
+ ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>();
+ ReplaceStringsVisitor visitor = new ReplaceStringsVisitor(
+ ast, astRewrite, astEditGroups,
+ tokenString, Rqualifier, xmlStringId);
+ node.accept(visitor);
+
+ // Finally prepare the change set
+ try {
+ MultiTextEdit edit = new MultiTextEdit();
+
+ // Create the edit to change the imports, only if anything changed
+ TextEdit subEdit = importRewrite.rewriteImports(subMonitor.newChild(1));
+ if (subEdit.hasChildren()) {
+ edit.addChild(subEdit);
+ }
+
+ // Create the edit to change the Java source, only if anything changed
+ subEdit = astRewrite.rewriteAST();
+ if (subEdit.hasChildren()) {
+ edit.addChild(subEdit);
+ }
+
+ // Only create a change set if any edit was collected
+ if (edit.hasChildren()) {
+ change.setEdit(edit);
+
+ // Create TextEditChangeGroups which let the user turn changes on or off
+ // individually. This must be done after the change.setEdit() call above.
+ for (TextEditGroup editGroup : astEditGroups) {
+ change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
+ }
+
+ changes.add(change);
+ }
+
+ // TODO to modify another Java source, loop back to the creation of the
+ // TextFileChange and accumulate in changes. Right now only one source is
+ // modified.
+
+ if (changes.size() > 0) {
+ return changes;
+ }
+
+ subMonitor.worked(1);
+
+ } catch (CoreException e) {
+ // ImportRewrite.rewriteImports failed.
+ status.addFatalError(e.getMessage());
+ }
+ return null;
+ }
+
+ public class ReplaceStringsVisitor extends ASTVisitor {
+
+ private final AST mAst;
+ private final ASTRewrite mRewriter;
+ private final String mOldString;
+ private final String mRQualifier;
+ private final String mXmlId;
+ private final ArrayList<TextEditGroup> mEditGroups;
+
+ public ReplaceStringsVisitor(AST ast,
+ ASTRewrite astRewrite,
+ ArrayList<TextEditGroup> editGroups,
+ String oldString,
+ String rQualifier,
+ String xmlId) {
+ mAst = ast;
+ mRewriter = astRewrite;
+ mEditGroups = editGroups;
+ mOldString = oldString;
+ mRQualifier = rQualifier;
+ mXmlId = xmlId;
+ }
+
+ @Override
+ public boolean visit(StringLiteral node) {
+ if (node.getLiteralValue().equals(mOldString)) {
+
+ Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$
+ SimpleName idName = mAst.newSimpleName(mXmlId);
+ QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName);
+
+ TextEditGroup editGroup = new TextEditGroup("Replace string by ID");
+ mEditGroups.add(editGroup);
+ mRewriter.replace(node, newNode, editGroup);
+ }
+ return super.visit(node);
+ }
+ }
+
+ /**
+ * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
+ * work and creates a descriptor that can be used to replay that refactoring later.
+ *
+ * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
+ *
+ * @throws CoreException
+ */
+ @Override
+ public Change createChange(IProgressMonitor monitor)
+ throws CoreException, OperationCanceledException {
+
+ try {
+ monitor.beginTask("Applying changes...", 1);
+
+ CompositeChange change = new CompositeChange(
+ getName(),
+ mChanges.toArray(new Change[mChanges.size()])) {
+ @Override
+ public ChangeDescriptor getDescriptor() {
+
+ String comment = String.format(
+ "Extracts string '%1$s' into R.string.%2$s",
+ mTokenString,
+ mXmlStringId);
+
+ ExtractStringDescriptor desc = new ExtractStringDescriptor(
+ mUnit.getJavaProject().getElementName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+
+ return new RefactoringChangeDescriptor(desc);
+ }
+ };
+
+ monitor.worked(1);
+
+ return change;
+
+ } finally {
+ monitor.done();
+ }
+
+ }
+
+ /**
+ * Utility method used by the wizard to check whether the given string ID is already
+ * defined in the XML file which path is given.
+ *
+ * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
+ * The given file may or may not exist.
+ * @param stringId The string ID to find.
+ * @return True if such a string ID is already defined.
+ */
+ public boolean isResIdDuplicate(String xmlFileWsPath, String stringId) {
+ // This is going to be called many times on the same file.
+ // Build a cache of the existing IDs for a given file.
+ if (mResIdCache == null) {
+ mResIdCache = new HashMap<String, HashSet<String>>();
+ }
+ HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
+ if (cache == null) {
+ cache = getResIdsForFile(xmlFileWsPath);
+ mResIdCache.put(xmlFileWsPath, cache);
+ }
+
+ return cache.contains(stringId);
+ }
+
+ /**
+ * Extract all the defined string IDs from a given file using XPath.
+ *
+ * @param xmlFileWsPath The project path of the file to parse. It may not exist.
+ * @return The set of all string IDs defined in the file. The returned set is always non
+ * null. It is empty if the file does not exist.
+ */
+ private HashSet<String> getResIdsForFile(String xmlFileWsPath) {
+ HashSet<String> ids = new HashSet<String>();
+
+ if (mXPath == null) {
+ mXPath = AndroidXPathFactory.newXPath();
+ }
+
+ // Access the project that contains the resource that contains the compilation unit
+ IResource resource = getTargetXmlResource(xmlFileWsPath);
+
+ if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
+ InputSource source;
+ try {
+ source = new InputSource(((IFile) resource).getContents());
+
+ // We want all the IDs in an XML structure like this:
+ // <resources>
+ // <string name="ID">something</string>
+ // </resources>
+
+ String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$
+
+ Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
+ if (result instanceof NodeList) {
+ NodeList list = (NodeList) result;
+ for (int n = list.getLength() - 1; n >= 0; n--) {
+ String id = list.item(n).getNodeValue();
+ ids.add(id);
+ }
+ }
+
+ } catch (CoreException e1) {
+ // IFile.getContents failed. Ignore.
+ } catch (XPathExpressionException e) {
+ // mXPath.evaluate failed. Ignore.
+ }
+ }
+
+ return ids;
+ }
+
+ /**
+ * Given a file project path, returns its resource in the same project than the
+ * compilation unit. The resource may not exist.
+ */
+ private IResource getTargetXmlResource(String xmlFileWsPath) {
+ IProject proj = mFile.getProject();
+ IResource resource = proj.getFile(xmlFileWsPath);
+ 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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java
new file mode 100644
index 000000000..c5b0c7d11
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.refactorings.extractstring;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+
+/**
+ * A wizard for ExtractString based on a simple dialog with one page.
+ *
+ * @see ExtractStringInputPage
+ * @see ExtractStringRefactoring
+ */
+class ExtractStringWizard extends RefactoringWizard {
+
+ private final IProject mProject;
+
+ /**
+ * Create a wizard for ExtractString based on a simple dialog with one page.
+ *
+ * @param ref The instance of {@link ExtractStringRefactoring} to associate to the wizard.
+ * @param project The project where the wizard was invoked from (e.g. where the user selection
+ * happened, so that we can retrieve project resources.)
+ */
+ public ExtractStringWizard(ExtractStringRefactoring ref, IProject project) {
+ super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mProject = project;
+ setDefaultPageTitle(ref.getName());
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ addPage(new ExtractStringInputPage(mProject));
+ }
+
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java
index dc600d7f7..19ef16cfa 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java
@@ -157,7 +157,7 @@ public class LayoutParamsParser {
superClasses[2] = paramsClassName;
}
HashMap<String, ArrayList<IClassDescriptor>> found =
- mClassLoader.findClassesDerivingFrom("android.", superClasses);
+ mClassLoader.findClassesDerivingFrom("android.", superClasses); //$NON-NLS-1$
mTopViewClass = mClassLoader.getClass(rootClassName);
mTopGroupClass = mClassLoader.getClass(groupClassName);
if (paramsClassName != null) {
@@ -179,8 +179,7 @@ public class LayoutParamsParser {
addView(mTopViewClass);
// ViewGroup derives from View
- mGroupMap.get(groupClassName).setSuperClass(
- mViewMap.get(rootClassName));
+ mGroupMap.get(groupClassName).setSuperClass(mViewMap.get(rootClassName));
progress.setWorkRemaining(mGroupList.size() + mViewList.size());
@@ -346,7 +345,7 @@ public class LayoutParamsParser {
private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) {
IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses();
for (IClassDescriptor innerClass : innerClasses) {
- if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_LAYOUTPARAMS)) {
+ if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_NAME_LAYOUTPARAMS)) {
return innerClass;
}
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
index 0dd88c077..e26b31cc3 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
@@ -22,7 +22,9 @@
package com.android.ide.eclipse.adt.wizards.newproject;
+import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.AndroidManifestParser;
import com.android.sdklib.IAndroidTarget;
@@ -122,6 +124,7 @@ public class NewProjectCreationPage extends WizardPage {
private Button mCreateActivityCheck;
private Text mMinSdkVersionField;
private SdkTargetSelector mSdkTargetSelector;
+ private ITargetChangeListener mSdkTargetChangeListener;
private boolean mInternalLocationPathUpdate;
protected boolean mInternalProjectNameUpdate;
@@ -263,6 +266,17 @@ public class NewProjectCreationPage extends WizardPage {
// Validate. This will complain about the first empty field.
setPageComplete(validatePage());
}
+
+ @Override
+ public void dispose() {
+
+ if (mSdkTargetChangeListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
+ mSdkTargetChangeListener = null;
+ }
+
+ super.dispose();
+ }
/**
* Creates the group for the project name:
@@ -389,18 +403,35 @@ public class NewProjectCreationPage extends WizardPage {
group.setFont(parent.getFont());
group.setText("Target");
- // get the targets from the sdk
- IAndroidTarget[] targets = null;
- if (Sdk.getCurrent() != null) {
- targets = Sdk.getCurrent().getTargets();
- }
+ // The selector is created without targets. They are added below in the change listener.
+ mSdkTargetSelector = new SdkTargetSelector(group, null, false /*multi-selection*/);
- mSdkTargetSelector = new SdkTargetSelector(group, targets, false /*multi-selection*/);
+ mSdkTargetChangeListener = new ITargetChangeListener() {
+ public void onProjectTargetChange(IProject changedProject) {
+ // Ignore
+ }
- // If there's only one target, select it
- if (targets != null && targets.length == 1) {
- mSdkTargetSelector.setSelection(targets[0]);
- }
+ public void onTargetsLoaded() {
+ // Update the sdk target selector with the new targets
+
+ // get the targets from the sdk
+ IAndroidTarget[] targets = null;
+ if (Sdk.getCurrent() != null) {
+ targets = Sdk.getCurrent().getTargets();
+ }
+ mSdkTargetSelector.setTargets(targets);
+
+ // If there's only one target, select it
+ if (targets != null && targets.length == 1) {
+ mSdkTargetSelector.setSelection(targets[0]);
+ }
+ }
+ };
+
+ AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
+
+ // Invoke it once to initialize the targets
+ mSdkTargetChangeListener.onTargetsLoaded();
mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
@Override
@@ -829,7 +860,7 @@ public class NewProjectCreationPage extends WizardPage {
String packageName = null;
String activityName = null;
- int minSdkVersion = 0; // 0 means no minSdkVersion provided in the manifest
+ int minSdkVersion = AndroidManifestParser.INVALID_MIN_SDK;
try {
packageName = manifestData.getPackage();
minSdkVersion = manifestData.getApiLevelRequirement();
@@ -927,7 +958,7 @@ public class NewProjectCreationPage extends WizardPage {
}
}
- if (!foundTarget && minSdkVersion > 0) {
+ if (!foundTarget && minSdkVersion != AndroidManifestParser.INVALID_MIN_SDK) {
try {
for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
if (target.getApiVersionNumber() == minSdkVersion) {
@@ -954,7 +985,8 @@ public class NewProjectCreationPage extends WizardPage {
if (!foundTarget) {
mInternalMinSdkVersionUpdate = true;
mMinSdkVersionField.setText(
- minSdkVersion <= 0 ? "" : Integer.toString(minSdkVersion)); //$NON-NLS-1$
+ minSdkVersion == AndroidManifestParser.INVALID_MIN_SDK ? "" :
+ Integer.toString(minSdkVersion)); //$NON-NLS-1$
mInternalMinSdkVersionUpdate = false;
}
}
@@ -1148,7 +1180,7 @@ public class NewProjectCreationPage extends WizardPage {
return MSG_NONE;
}
- int version = -1;
+ int version = AndroidManifestParser.INVALID_MIN_SDK;
try {
// If not empty, it must be a valid integer > 0
version = Integer.parseInt(getMinSdkVersion());
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java
index 5abfd811d..226357f7a 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java
+++ b/tools/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$
@@ -187,14 +189,16 @@ public class AndroidConstants {
public final static String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$
public final static String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$
public final static String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$
- public final static String CLASS_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
+ public final static String CLASS_NAME_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
public final static String CLASS_VIEWGROUP_LAYOUTPARAMS =
- CLASS_VIEWGROUP + "$" + CLASS_LAYOUTPARAMS; //$NON-NLS-1$
- public final static String CLASS_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
+ CLASS_VIEWGROUP + "$" + CLASS_NAME_LAYOUTPARAMS; //$NON-NLS-1$
+ public final static String CLASS_NAME_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
+ public final static String CLASS_FRAMELAYOUT =
+ "android.widget." + CLASS_NAME_FRAMELAYOUT; //$NON-NLS-1$
public final static String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$
- public final static String CLASS_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
+ public final static String CLASS_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
public final static String CLASS_PREFERENCES =
- "android.preference." + CLASS_PREFERENCE_SCREEN; //$NON-NLS-1$
+ "android.preference." + CLASS_NAME_PREFERENCE_SCREEN; //$NON-NLS-1$
public final static String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$
public final static String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$
@@ -215,4 +219,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/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
index 0a45196a4..85ba96839 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
+++ b/tools/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;
@@ -33,6 +34,7 @@ import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
@@ -72,6 +74,8 @@ public class AndroidManifestParser {
private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$
private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$
+ public final static int INVALID_MIN_SDK = -1;
+
/**
* XML error & data handler used when parsing the AndroidManifest.xml file.
* <p/>
@@ -92,8 +96,9 @@ public class AndroidManifestParser {
private Set<String> mProcesses = null;
/** debuggable attribute value. If null, the attribute is not present. */
private Boolean mDebuggable = null;
- /** API level requirement. if 0 the attribute was not present. */
- private int mApiLevelRequirement = 0;
+ /** API level requirement. if {@link AndroidManifestParser#INVALID_MIN_SDK}
+ * the attribute was not present. */
+ private int mApiLevelRequirement = INVALID_MIN_SDK;
/** List of all instrumentations declared by the manifest */
private final ArrayList<String> mInstrumentations = new ArrayList<String>();
/** List of all libraries in use declared by the manifest */
@@ -171,7 +176,8 @@ public class AndroidManifestParser {
}
/**
- * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
+ * Returns the <code>minSdkVersion</code> attribute, or
+ * {@link AndroidManifestParser#INVALID_MIN_SDK} if it's not set.
*/
int getApiLevelRequirement() {
return mApiLevelRequirement;
@@ -251,12 +257,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:
@@ -445,6 +447,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.
@@ -480,8 +501,7 @@ public class AndroidManifestParser {
} catch (CoreException e) {
}
}
- }
-
+ }
}
/**
@@ -561,25 +581,41 @@ 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
return new AndroidManifestParser(manifestHandler.getPackage(),
- manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
- manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
- manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
+ manifestHandler.getActivities(),
+ manifestHandler.getLauncherActivity(),
+ manifestHandler.getProcesses(),
+ manifestHandler.getDebuggable(),
+ manifestHandler.getApiLevelRequirement(),
+ manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "Bad parser configuration for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
} catch (SAXException e) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "Parser exception for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
} catch (IOException e) {
- } finally {
- }
+ // Don't log a console error when failing to read a non-existing file
+ if (!(e instanceof FileNotFoundException)) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "I/O error for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
+ }
+ }
return null;
}
-
+
/**
* Parses the Android Manifest, and returns an object containing the result of the parsing.
* <p/>
@@ -610,16 +646,33 @@ public class AndroidManifestParser {
// get the result from the handler
return new AndroidManifestParser(manifestHandler.getPackage(),
- manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
- manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
- manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(),
+ manifestHandler.getActivities(),
+ manifestHandler.getLauncherActivity(),
+ manifestHandler.getProcesses(),
+ manifestHandler.getDebuggable(),
+ manifestHandler.getApiLevelRequirement(),
+ manifestHandler.getInstrumentations(),
manifestHandler.getUsesLibraries());
} catch (ParserConfigurationException e) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "Bad parser configuration for %s: %s",
+ manifestFile.getAbsolutePath(),
+ e.getMessage());
} catch (SAXException e) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "Parser exception for %s: %s",
+ manifestFile.getAbsolutePath(),
+ e.getMessage());
} catch (IOException e) {
- } finally {
+ // Don't log a console error when failing to read a non-existing file
+ if (!(e instanceof FileNotFoundException)) {
+ AdtPlugin.logAndPrintError(e, AndroidManifestParser.class.getCanonicalName(),
+ "I/O error for %s: %s",
+ manifestFile.getAbsolutePath(),
+ e.getMessage());
+ }
}
-
+
return null;
}
@@ -642,10 +695,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);
@@ -660,10 +715,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;
}
@@ -750,7 +810,8 @@ public class AndroidManifestParser {
}
/**
- * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
+ * Returns the <code>minSdkVersion</code> attribute, or {@link #INVALID_MIN_SDK}
+ * if it's not set.
*/
public int getApiLevelRequirement() {
return mApiLevelRequirement;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
index 0c43499b9..b6d4c9ac7 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
@@ -16,8 +16,7 @@
package com.android.ide.eclipse.common.project;
-import com.android.ide.eclipse.common.project.BaseProjectHelper;
-
+import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jdt.core.IJavaModel;
@@ -82,7 +81,7 @@ public class ProjectChooserHelper {
// open the dialog and return the object selected if OK was clicked, or null otherwise
if (dialog.open() == Window.OK) {
- return (IJavaProject)dialog.getFirstResult();
+ return (IJavaProject) dialog.getFirstResult();
}
return null;
}
@@ -107,4 +106,24 @@ public class ProjectChooserHelper {
return mAndroidProjects;
}
+
+ /**
+ * Helper method to get the Android project with the given name
+ *
+ * @param projectName the name of the project to find
+ * @return the {@link IProject} for the Android project. <code>null</code> if not found.
+ */
+ public IProject getAndroidProject(String projectName) {
+ IProject iproject = null;
+ IJavaProject[] javaProjects = getAndroidProjects(null);
+ if (javaProjects != null) {
+ for (IJavaProject javaProject : javaProjects) {
+ if (javaProject.getElementName().equals(projectName)) {
+ iproject = javaProject.getProject();
+ break;
+ }
+ }
+ }
+ return iproject;
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java
index efa5981b5..7aad7c84a 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java
@@ -70,6 +70,18 @@ public class DeclareStyleableInfo {
mFormats = formats;
}
+ /**
+ * @param name The XML Name of the attribute
+ * @param formats The formats of the attribute. Cannot be null.
+ * Should have at least one format.
+ * @param javadoc Short javadoc (i.e. the first sentence).
+ */
+ public AttributeInfo(String name, Format[] formats, String javadoc) {
+ mName = name;
+ mFormats = formats;
+ mJavaDoc = javadoc;
+ }
+
public AttributeInfo(AttributeInfo info) {
mName = info.mName;
mFormats = info.mFormats;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java
index 555015537..6a9b7db29 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java
@@ -24,6 +24,7 @@ import com.android.sdklib.SdkConstants;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.graphics.Image;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -221,6 +222,18 @@ public class ElementDescriptor {
mChildren = newChildren;
}
+ /** Sets the list of allowed children.
+ * <p/>
+ * This is just a convenience method that converts a Collection into an array and
+ * calls {@link #setChildren(ElementDescriptor[])}.
+ * <p/>
+ * This means a <em>copy</em> of the collection is made. The collection is not
+ * stored by the recipient and can thus be altered by the caller.
+ */
+ public void setChildren(Collection<ElementDescriptor> newChildren) {
+ setChildren(newChildren.toArray(new ElementDescriptor[newChildren.size()]));
+ }
+
/**
* Returns an optional tooltip. Will be null if not present.
* <p/>
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java
index 9c529e5da..12d49fe27 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java
+++ b/tools/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}.
- * <p/>If a qualifier is not present in the {@link FolderConfiguration} object, the UI control
- * is not modified. However if the value in the control is not the default value, a warning
- * icon is showed.
+ * <p/>If <var>force</var> is set to <code>true</code> the UI will be changed to exactly reflect
+ * <var>config</var>, otherwise, if a qualifier is not present in <var>config</var>,
+ * the UI control is not modified. However if the value in the control is not the default value,
+ * a warning icon is shown.
+ * @param config The {@link FolderConfiguration} to set.
+ * @param force Whether the UI should be changed to exactly match the received configuration.
*/
- void setConfiguration(FolderConfiguration config) {
+ void setConfiguration(FolderConfiguration config, boolean force) {
mDisableUpdates = true; // we do not want to trigger onXXXChange when setting new values in the widgets.
mEditedConfig = config;
@@ -1088,6 +1089,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (countryQualifier != null) {
mCountry.setText(String.format("%1$d", countryQualifier.getCode()));
mCurrentConfig.setCountryCodeQualifier(countryQualifier);
+ } else if (force) {
+ mCountry.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setCountryCodeQualifier(null);
} else if (mCountry.getText().length() > 0) {
mCountryIcon.setImage(mWarningImage);
}
@@ -1097,6 +1101,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (networkQualifier != null) {
mNetwork.setText(String.format("%1$d", networkQualifier.getCode()));
mCurrentConfig.setNetworkCodeQualifier(networkQualifier);
+ } else if (force) {
+ mNetwork.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setNetworkCodeQualifier(null);
} else if (mNetwork.getText().length() > 0) {
mNetworkIcon.setImage(mWarningImage);
}
@@ -1106,6 +1113,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (languageQualifier != null) {
mLanguage.setText(languageQualifier.getValue());
mCurrentConfig.setLanguageQualifier(languageQualifier);
+ } else if (force) {
+ mLanguage.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setLanguageQualifier(null);
} else if (mLanguage.getText().length() > 0) {
mLanguageIcon.setImage(mWarningImage);
}
@@ -1115,6 +1125,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (regionQualifier != null) {
mRegion.setText(regionQualifier.getValue());
mCurrentConfig.setRegionQualifier(regionQualifier);
+ } else if (force) {
+ mRegion.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setRegionQualifier(null);
} else if (mRegion.getText().length() > 0) {
mRegionIcon.setImage(mWarningImage);
}
@@ -1125,6 +1138,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mOrientation.select(
ScreenOrientation.getIndex(orientationQualifier.getValue()) + 1);
mCurrentConfig.setScreenOrientationQualifier(orientationQualifier);
+ } else if (force) {
+ mOrientation.select(0);
+ mCurrentConfig.setScreenOrientationQualifier(null);
} else if (mOrientation.getSelectionIndex() != 0) {
mOrientationIcon.setImage(mWarningImage);
}
@@ -1134,6 +1150,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (densityQualifier != null) {
mDensity.setText(String.format("%1$d", densityQualifier.getValue()));
mCurrentConfig.setPixelDensityQualifier(densityQualifier);
+ } else if (force) {
+ mDensity.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setPixelDensityQualifier(null);
} else if (mDensity.getText().length() > 0) {
mDensityIcon.setImage(mWarningImage);
}
@@ -1143,6 +1162,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (touchQualifier != null) {
mTouch.select(TouchScreenType.getIndex(touchQualifier.getValue()) + 1);
mCurrentConfig.setTouchTypeQualifier(touchQualifier);
+ } else if (force) {
+ mTouch.select(0);
+ mCurrentConfig.setTouchTypeQualifier(null);
} else if (mTouch.getSelectionIndex() != 0) {
mTouchIcon.setImage(mWarningImage);
}
@@ -1152,6 +1174,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (keyboardQualifier != null) {
mKeyboard.select(KeyboardState.getIndex(keyboardQualifier.getValue()) + 1);
mCurrentConfig.setKeyboardStateQualifier(keyboardQualifier);
+ } else if (force) {
+ mKeyboard.select(0);
+ mCurrentConfig.setKeyboardStateQualifier(null);
} else if (mKeyboard.getSelectionIndex() != 0) {
mKeyboardIcon.setImage(mWarningImage);
}
@@ -1161,6 +1186,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
if (inputQualifier != null) {
mTextInput.select(TextInputMethod.getIndex(inputQualifier.getValue()) + 1);
mCurrentConfig.setTextInputMethodQualifier(inputQualifier);
+ } else if (force) {
+ mTextInput.select(0);
+ mCurrentConfig.setTextInputMethodQualifier(null);
} else if (mTextInput.getSelectionIndex() != 0) {
mTextInputIcon.setImage(mWarningImage);
}
@@ -1171,6 +1199,9 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mNavigation.select(
NavigationMethod.getIndex(navigationQualifiter.getValue()) + 1);
mCurrentConfig.setNavigationMethodQualifier(navigationQualifiter);
+ } else if (force) {
+ mNavigation.select(0);
+ mCurrentConfig.setNavigationMethodQualifier(null);
} else if (mNavigation.getSelectionIndex() != 0) {
mNavigationIcon.setImage(mWarningImage);
}
@@ -1181,6 +1212,10 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
mSize1.setText(String.format("%1$d", sizeQualifier.getValue1()));
mSize2.setText(String.format("%1$d", sizeQualifier.getValue2()));
mCurrentConfig.setScreenDimensionQualifier(sizeQualifier);
+ } else if (force) {
+ mSize1.setText(""); //$NON-NLS-1$
+ mSize2.setText(""); //$NON-NLS-1$
+ mCurrentConfig.setScreenDimensionQualifier(null);
} else if (mSize1.getText().length() > 0 && mSize2.getText().length() > 0) {
mSizeIcon.setImage(mWarningImage);
}
@@ -1607,7 +1642,7 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// at this point, we have not opened a new file.
// update the configuration icons with the new edited config.
- setConfiguration(mEditedConfig);
+ setConfiguration(mEditedConfig, false /*force*/);
// enable the create button if the current and edited config are not equals
mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
@@ -1794,45 +1829,16 @@ public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor
// Compute the layout
UiElementPullParser parser = new UiElementPullParser(getModel());
Rectangle rect = getBounds();
- ILayoutResult result = null;
- if (bridge.apiLevel >= 3) {
- // call the new api with proper theme differentiator and
- // density/dpi support.
- boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
-
- // 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<String, Map<String, IResourceValue>> projectResources,
+ Map<String, Map<String, IResourceValue>> frameworkResources,
+ IProjectCallback projectCallback, ILayoutLog logger) {
+
+ if (bridge.apiLevel >= 3) {
+ // newer api with boolean for separation of project/framework theme,
+ // and density support.
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, density, xdpi, ydpi,
+ themeName, isProjectTheme,
+ projectResources, frameworkResources, projectCallback,
+ logger);
+ } else if (bridge.apiLevel == 2) {
+ // api with boolean for separation of project/framework theme
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, themeName, isProjectTheme,
+ mConfiguredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+ } else {
+ // oldest api with no density/dpi, and project theme boolean mixed
+ // into the theme name.
+
+ // change the string if it's a custom theme to make sure we can
+ // differentiate them
+ if (isProjectTheme) {
+ themeName = "*" + themeName; //$NON-NLS-1$
+ }
+
+ return bridge.bridge.computeLayout(layoutDescription,
+ projectKey, screenWidth, screenHeight, themeName,
+ mConfiguredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+ }
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java
index 5726d7899..a59ad6f19 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java
@@ -43,7 +43,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
public static final String ID_ATTR = "id"; //$NON-NLS-1$
/** The document descriptor. Contains all layouts and views linked together. */
- private DocumentDescriptor mDescriptor =
+ private DocumentDescriptor mRootDescriptor =
new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$
/** The list of all known ViewLayout descriptors. */
@@ -60,7 +60,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
/** @return the document descriptor. Contains all layouts and views linked together. */
public DocumentDescriptor getDescriptor() {
- return mDescriptor;
+ return mRootDescriptor;
}
/** @return The read-only list of all known ViewLayout descriptors. */
@@ -74,7 +74,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
}
public ElementDescriptor[] getRootElementDescriptors() {
- return mDescriptor.getChildren();
+ return mRootDescriptor.getChildren();
}
/**
@@ -98,6 +98,10 @@ public final class LayoutDescriptors implements IDescriptorProvider {
}
}
+ // Create <include> as a synthetic regular view.
+ // Note: ViewStub is already described by attrs.xml
+ insertInclude(newViews);
+
ArrayList<ElementDescriptor> newLayouts = new ArrayList<ElementDescriptor>();
if (layouts != null) {
for (ViewClassInfo info : layouts) {
@@ -109,17 +113,22 @@ public final class LayoutDescriptors implements IDescriptorProvider {
ArrayList<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>();
newDescriptors.addAll(newLayouts);
newDescriptors.addAll(newViews);
- ElementDescriptor[] newArray = newDescriptors.toArray(
- new ElementDescriptor[newDescriptors.size()]);
// Link all layouts to everything else here.. recursively
for (ElementDescriptor layoutDesc : newLayouts) {
- layoutDesc.setChildren(newArray);
+ layoutDesc.setChildren(newDescriptors);
}
+ // The <merge> tag can only be a root tag, so it is added at the end.
+ // It gets everything else as children but it is not made a child itself.
+ ElementDescriptor mergeTag = createMerge(newLayouts);
+ mergeTag.setChildren(newDescriptors); // mergeTag makes a copy of the list
+ newDescriptors.add(mergeTag);
+ newLayouts.add(mergeTag);
+
mViewDescriptors = newViews;
mLayoutDescriptors = newLayouts;
- mDescriptor.setChildren(newArray);
+ mRootDescriptor.setChildren(newDescriptors);
mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors);
mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors);
@@ -186,7 +195,7 @@ public final class LayoutDescriptors implements IDescriptorProvider {
if (need_separator) {
String title;
if (layoutParams.getShortClassName().equals(
- AndroidConstants.CLASS_LAYOUTPARAMS)) {
+ AndroidConstants.CLASS_NAME_LAYOUTPARAMS)) {
title = String.format("Layout Attributes from %1$s",
layoutParams.getViewLayoutClass().getShortClassName());
} else {
@@ -217,4 +226,99 @@ public final class LayoutDescriptors implements IDescriptorProvider {
false /* mandatory */);
}
+ /**
+ * Creates a new <include> descriptor and adds it to the list of view descriptors.
+ *
+ * @param knownViews A list of view descriptors being populated. Also used to find the
+ * View descriptor and extract its layout attributes.
+ */
+ private void insertInclude(ArrayList<ElementDescriptor> knownViews) {
+ String xml_name = "include"; //$NON-NLS-1$
+
+ // Create the include custom attributes
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
+
+ // Note that the "layout" attribute does NOT have the Android namespace
+ DescriptorsUtils.appendAttribute(attributes,
+ null, //elementXmlName
+ null, //nsUri
+ new AttributeInfo(
+ "layout", //$NON-NLS-1$
+ new AttributeInfo.Format[] { AttributeInfo.Format.REFERENCE }
+ ),
+ true, //required
+ null); //overrides
+
+ DescriptorsUtils.appendAttribute(attributes,
+ null, //elementXmlName
+ SdkConstants.NS_RESOURCES, //nsUri
+ new AttributeInfo(
+ "id", //$NON-NLS-1$
+ new AttributeInfo.Format[] { AttributeInfo.Format.REFERENCE }
+ ),
+ true, //required
+ null); //overrides
+
+ // Find View and inherit all its layout attributes
+ AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
+ AndroidConstants.CLASS_VIEW, knownViews);
+
+ // Create the include descriptor
+ ViewElementDescriptor desc = new ViewElementDescriptor(xml_name, // xml_name
+ xml_name, // ui_name
+ null, // canonical class name, we don't have one
+ "Lets you statically include XML layouts inside other XML layouts.", // tooltip
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ knownViews.add(desc);
+ }
+
+ /**
+ * Creates and return a new <merge> descriptor.
+ * @param knownLayouts A list of all known layout view descriptors, used to find the
+ * FrameLayout descriptor and extract its layout attributes.
+ */
+ private ElementDescriptor createMerge(ArrayList<ElementDescriptor> knownLayouts) {
+ String xml_name = "merge"; //$NON-NLS-1$
+
+ // Find View and inherit all its layout attributes
+ AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
+ AndroidConstants.CLASS_FRAMELAYOUT, knownLayouts);
+
+ // Create the include descriptor
+ ViewElementDescriptor desc = new ViewElementDescriptor(xml_name, // xml_name
+ xml_name, // ui_name
+ null, // canonical class name, we don't have one
+ "A root tag useful for XML layouts inflated using a ViewStub.", // tooltip
+ null, // sdk_url
+ null, // attributes
+ viewLayoutAttribs, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ return desc;
+ }
+
+ /**
+ * Finds the descriptor and retrieves all its layout attributes.
+ */
+ private AttributeDescriptor[] findViewLayoutAttributes(
+ String viewFqcn,
+ ArrayList<ElementDescriptor> knownViews) {
+
+ for (ElementDescriptor desc : knownViews) {
+ if (desc instanceof ViewElementDescriptor) {
+ ViewElementDescriptor viewDesc = (ViewElementDescriptor) desc;
+ if (viewFqcn.equals(viewDesc.getCanonicalClassName())) {
+ return viewDesc.getLayoutAttributes();
+ }
+ }
+ }
+
+ return null;
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java
index 6e79d64d4..b7d69af06 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java
@@ -96,6 +96,8 @@ class DropFeedback {
RelativeInfo info = null;
UiElementEditPart sibling = null;
+ // TODO consider merge like a vertical layout
+ // TODO consider TableLayout like a linear
if (LayoutConstants.LINEAR_LAYOUT.equals(layoutXmlName)) {
sibling = findLinearTarget(parentPart, where)[1];
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java
index 1bf5d5abf..738591a29 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java
@@ -81,7 +81,7 @@ public class UiViewElementNode extends UiElementNode {
if (layoutDescriptors != null) {
for (ElementDescriptor desc : layoutDescriptors) {
if (desc instanceof ViewElementDescriptor &&
- desc.getXmlName().equals(AndroidConstants.CLASS_FRAMELAYOUT)) {
+ desc.getXmlName().equals(AndroidConstants.CLASS_NAME_FRAMELAYOUT)) {
layout_attrs = ((ViewElementDescriptor) desc).getLayoutAttributes();
need_xmlns = true;
break;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java
index 3728886b5..517284c83 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java
@@ -1233,8 +1233,9 @@ public class UiElementNode implements IPropertySource {
Node attr = attrs.item(n);
if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$
String uri = attr.getNodeValue();
- String nsPrefix = attr.getLocalName();
- if (SdkConstants.NS_RESOURCES.equals(uri)) {
+ String nsPrefix = attr.getLocalName();
+ // Is this the URI we are looking for? If yes, we found its prefix.
+ if (nsUri.equals(uri)) {
return nsPrefix;
}
visited.add(nsPrefix);
@@ -1244,7 +1245,8 @@ public class UiElementNode implements IPropertySource {
// Use a sensible default prefix if we can't find one.
// We need to make sure the prefix is not one that was declared in the scope
- // visited above.
+ // visited above. Use a default namespace prefix "android" for the Android resource
+ // NS and use "ns" for all other custom namespaces.
String prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$
String base = prefix;
for (int i = 1; visited.contains(prefix); i++) {
@@ -1475,18 +1477,18 @@ public class UiElementNode implements IPropertySource {
}
}
}
-
+
if (attribute != null) {
- final UiAttributeNode fAttribute = attribute;
// get the current value and compare it to the new value
- String oldValue = fAttribute.getCurrentValue();
+ String oldValue = attribute.getCurrentValue();
final String newValue = (String)value;
if (oldValue.equals(newValue)) {
return;
}
-
+
+ final UiAttributeNode fAttribute = attribute;
AndroidEditor editor = getEditor();
editor.editXmlModel(new Runnable() {
public void run() {
@@ -1495,6 +1497,4 @@ public class UiElementNode implements IPropertySource {
});
}
}
-
-
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java
index e84c05123..83ab59be1 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java
@@ -17,8 +17,10 @@
package com.android.ide.eclipse.editors.wizards;
+import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.ProjectChooserHelper;
import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
@@ -39,6 +41,7 @@ import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.viewers.IStructuredSelection;
@@ -235,7 +238,7 @@ class NewXmlFileCreationPage extends WizardPage {
"An XML file that describes preferences.", // tooltip
ResourceFolderType.XML, // folder type
AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed
- AndroidConstants.CLASS_PREFERENCE_SCREEN, // default root
+ AndroidConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root
SdkConstants.NS_RESOURCES, // xmlns
null, // default attributes
1 // target API level
@@ -271,9 +274,9 @@ class NewXmlFileCreationPage extends WizardPage {
final static int NUM_COL = 4;
/** Absolute destination folder root, e.g. "/res/" */
- private static String sResFolderAbs = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
+ private static final String RES_FOLDER_ABS = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
/** Relative destination folder root, e.g. "res/" */
- private static String sResFolderRel = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
+ private static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
private IProject mProject;
private Text mProjectTextField;
@@ -288,7 +291,9 @@ class NewXmlFileCreationPage extends WizardPage {
private boolean mInternalTypeUpdate;
private boolean mInternalConfigSelectorUpdate;
private ProjectChooserHelper mProjectChooserHelper;
+ private ITargetChangeListener mSdkTargetChangeListener;
+ private TypeInfo mCurrentTypeInfo;
// --- UI creation ---
@@ -335,8 +340,43 @@ class NewXmlFileCreationPage extends WizardPage {
initializeFromSelection(mInitialSelection);
initializeRootValues();
enableTypesBasedOnApi();
+ if (mCurrentTypeInfo != null) {
+ updateRootCombo(mCurrentTypeInfo);
+ }
+ installTargetChangeListener();
validatePage();
}
+
+ private void installTargetChangeListener() {
+ mSdkTargetChangeListener = new ITargetChangeListener() {
+ public void onProjectTargetChange(IProject changedProject) {
+ // If this is the current project, force it to reload its data
+ if (changedProject != null && changedProject == mProject) {
+ changeProject(mProject);
+ }
+ }
+
+ public void onTargetsLoaded() {
+ // Reload the current project, if any, in case its target has changed.
+ if (mProject != null) {
+ changeProject(mProject);
+ }
+ }
+ };
+
+ AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);
+ }
+
+ @Override
+ public void dispose() {
+
+ if (mSdkTargetChangeListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
+ mSdkTargetChangeListener = null;
+ }
+
+ super.dispose();
+ }
/**
* Returns the target project or null.
@@ -650,7 +690,6 @@ class NewXmlFileCreationPage extends WizardPage {
return;
}
-
// Find the best match in the element list. In case there are multiple selected elements
// select the one that provides the most information and assign them a score,
// e.g. project=1 + folder=2 + file=4.
@@ -745,8 +784,35 @@ class NewXmlFileCreationPage extends WizardPage {
// cleared above.
// get the AndroidTargetData from the project
- IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
- AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ IAndroidTarget target = null;
+ AndroidTargetData data = null;
+
+ target = Sdk.getCurrent().getTarget(mProject);
+ if (target == null) {
+ // A project should have a target. The target can be missing if the project
+ // is an old project for which a target hasn't been affected or if the
+ // target no longer exists in this SDK. Simply log the error and dismiss.
+
+ AdtPlugin.log(IStatus.INFO,
+ "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$
+ mProject.getName());
+ continue;
+ } else {
+ data = Sdk.getCurrent().getTargetData(target);
+
+ if (data == null) {
+ // We should have both a target and its data.
+ // However if the wizard is invoked whilst the platform is still being
+ // loaded we can end up in a weird case where we have a target but it
+ // doesn't have any data yet.
+ // Lets log a warning and silently ignore this root.
+
+ AdtPlugin.log(IStatus.INFO,
+ "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$
+ target.getName(), mProject.getName());
+ continue;
+ }
+ }
IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
ElementDescriptor descriptor = provider.getDescriptor();
@@ -819,6 +885,10 @@ class NewXmlFileCreationPage extends WizardPage {
/**
* Changes mProject to the given new project and update the UI accordingly.
+ * <p/>
+ * Note that this does not check if the new project is the same as the current one
+ * on purpose, which allows a project to be updated when its target has changed or
+ * when targets are loaded in the background.
*/
private void changeProject(IProject newProject) {
mProject = newProject;
@@ -856,16 +926,16 @@ class NewXmlFileCreationPage extends WizardPage {
ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>();
// We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
- if (wsFolderPath.startsWith(sResFolderRel)) {
- wsFolderPath = sResFolderAbs + wsFolderPath.substring(sResFolderRel.length());
+ if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
+ wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
mInternalWsFolderPathUpdate = true;
mWsFolderPathTextField.setText(wsFolderPath);
mInternalWsFolderPathUpdate = false;
}
- if (wsFolderPath.startsWith(sResFolderAbs)) {
- wsFolderPath = wsFolderPath.substring(sResFolderAbs.length());
+ if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
if (pos >= 0) {
@@ -952,16 +1022,16 @@ class NewXmlFileCreationPage extends WizardPage {
// The configuration is valid. Reformat the folder path using the canonical
// value from the configuration.
- newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
+ newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
} else {
// The configuration is invalid. We still update the path but this time
// do it manually on the string.
- if (wsFolderPath.startsWith(sResFolderAbs)) {
+ if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
wsFolderPath.replaceFirst(
- "^(" + sResFolderAbs +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
+ "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
"\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
- newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
+ newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType());
}
}
@@ -1018,7 +1088,7 @@ class NewXmlFileCreationPage extends WizardPage {
if (type != null) {
mConfigSelector.getConfiguration(mTempConfig);
- StringBuffer sb = new StringBuffer(sResFolderAbs);
+ StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
sb.append(mTempConfig.getFolderName(type.getResFolderType()));
mInternalWsFolderPathUpdate = true;
@@ -1038,6 +1108,7 @@ class NewXmlFileCreationPage extends WizardPage {
private void selectType(TypeInfo type) {
if (type == null || !type.getWidget().getSelection()) {
mInternalTypeUpdate = true;
+ mCurrentTypeInfo = type;
for (TypeInfo type2 : sTypes) {
type2.getWidget().setSelection(type2 == type);
}
@@ -1131,8 +1202,8 @@ class NewXmlFileCreationPage extends WizardPage {
// -- validate generated path
if (error == null) {
String wsFolderPath = getWsFolderPath();
- if (!wsFolderPath.startsWith(sResFolderAbs)) {
- error = String.format("Target folder must start with %1$s.", sResFolderAbs);
+ if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+ error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS);
}
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java
index 262ef65a1..d86d585a3 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java
+++ b/tools/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/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java
index 516e448e6..7e8b0af10 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java
+++ b/tools/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("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
- sb.append("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"); //$NON-NLS-1$
- sb.append(" package=\""); //$NON-NLS-1$
- sb.append(PACKAGE_NAME);
- sb.append("\">\n"); //$NON-NLS-1$
- sb.append(" <application android:icon=\"@drawable/icon\">\n"); //$NON-NLS-1$
- sb.append(" <activity android:name=\""); //$NON-NLS-1$
- sb.append(ACTIVITY_NAME);
- sb.append("\" android:label=\"@string/app_name\">\n"); //$NON-NLS-1$
- sb.append(" <intent-filter>\n"); //$NON-NLS-1$
- sb.append(" <action android:name=\"android.intent.action.MAIN\" />\n"); //$NON-NLS-1$
- sb.append(" <category android:name=\"android.intent.category.LAUNCHER\" />\"\n"); //$NON-NLS-1$
- sb.append(" <category android:name=\"android.intent.category.DEFAULT\" />\n"); //$NON-NLS-1$
- sb.append(" </intent-filter>\n"); //$NON-NLS-1$
- sb.append(" </activity>\n"); //$NON-NLS-1$
- sb.append(" <uses-library android:name=\""); //$NON-NLS-1$
- sb.append(LIBRARY_NAME);
- sb.append("\" />\n"); //$NON-NLS-1$
- sb.append(" </application>"); //$NON-NLS-1$
- sb.append(" <instrumentation android:name=\""); //$NON-NLS-1$
- sb.append(INSTRUMENTATION_NAME);
- sb.append("\"\n");
- sb.append(" android:targetPackage=\"com.example.android.apis\"\n");
- sb.append(" android:label=\"Tests for Api Demos.\"/>\n");
- sb.append("</manifest>\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/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
index 987ea9247..573915366 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
@@ -461,5 +461,13 @@ public class FileMock implements IFile {
public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
}
+
+ public boolean isHidden(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isTeamPrivateMember(int options) {
+ throw new NotImplementedException();
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
index 73a69aad4..26bf53eeb 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
@@ -74,7 +74,8 @@ public final class FolderMock implements IFolder {
// -------- UNIMPLEMENTED METHODS ----------------
- public void create(boolean force, boolean local, IProgressMonitor monitor) throws CoreException {
+ public void create(boolean force, boolean local, IProgressMonitor monitor)
+ throws CoreException {
throw new NotImplementedException();
}
@@ -106,8 +107,8 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
- public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor)
- throws CoreException {
+ public void move(IPath destination, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
throw new NotImplementedException();
}
@@ -225,7 +226,8 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
- public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+ public void deleteMarkers(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
throw new NotImplementedException();
}
@@ -428,24 +430,31 @@ public final class FolderMock implements IFolder {
throw new NotImplementedException();
}
- public Map<?,?> getPersistentProperties() throws CoreException {
+ public Map<?,?> getPersistentProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Map<?,?> getSessionProperties() throws CoreException {
throw new NotImplementedException();
- }
+ }
- public Map<?,?> getSessionProperties() throws CoreException {
+ public boolean isDerived(int options) {
throw new NotImplementedException();
- }
+ }
- public boolean isDerived(int options) {
+ public boolean isHidden() {
throw new NotImplementedException();
- }
+ }
- public boolean isHidden() {
+ public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
- }
+ }
- public void setHidden(boolean isHidden) throws CoreException {
+ public boolean isHidden(int options) {
throw new NotImplementedException();
- }
+ }
+ public boolean isTeamPrivateMember(int options) {
+ throw new NotImplementedException();
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
index 0e6fde06d..6c32d0f3f 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
@@ -42,6 +42,14 @@ import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import java.net.URI;
import java.util.Map;
+/**
+ * Mock implementation of {@link IProject}.
+ * <p/>Supported methods:
+ * <ul>
+ * <li>{@link #build(int kind, IProgressMonitor monitor)}</li>
+ * <li>{@link #members(int kind, String builderName, Map args, IProgressMonitor monitor)}</li>
+ * </ul>
+ */
@SuppressWarnings("deprecation")
public class ProjectMock implements IProject {
@@ -265,7 +273,8 @@ public class ProjectMock implements IProject {
throw new NotImplementedException();
}
- public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+ public void deleteMarkers(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
throw new NotImplementedException();
}
@@ -473,29 +482,36 @@ public class ProjectMock implements IProject {
throw new NotImplementedException();
}
- public void create(IProjectDescription description, int updateFlags,
- IProgressMonitor monitor) throws CoreException {
+ public void create(IProjectDescription description, int updateFlags,
+ IProgressMonitor monitor) throws CoreException {
throw new NotImplementedException();
- }
+ }
+
+ public Map<?,?> getPersistentProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
- public Map<?,?> getPersistentProperties() throws CoreException {
+ public Map<?,?> getSessionProperties() throws CoreException {
throw new NotImplementedException();
- }
+ }
- public Map<?,?> getSessionProperties() throws CoreException {
+ public boolean isDerived(int options) {
throw new NotImplementedException();
- }
+ }
- public boolean isDerived(int options) {
+ public boolean isHidden() {
throw new NotImplementedException();
- }
+ }
- public boolean isHidden() {
+ public void setHidden(boolean isHidden) throws CoreException {
throw new NotImplementedException();
- }
+ }
- public void setHidden(boolean isHidden) throws CoreException {
+ public boolean isHidden(int options) {
throw new NotImplementedException();
- }
+ }
+ public boolean isTeamPrivateMember(int options) {
+ throw new NotImplementedException();
+ }
}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml
new file mode 100644
index 000000000..b380f967e
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-instrumentation.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.AndroidProject.tests">
+
+ <!--
+ This declares that this app uses the instrumentation test runner targeting
+ the package of com.android.samples. To run the tests use the command:
+ "adb shell am instrument -w com.android.samples.tests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.AndroidProject"
+ android:label="Sample test for deployment."/>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml
new file mode 100644
index 000000000..8ae70121c
--- /dev/null
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/AndroidManifest-testapp.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.testapp">
+ <application android:icon="@drawable/icon">
+ <activity android:name="com.android.testapp.MainActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />"
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <uses-library android:name="android.test.runner"/>
+ </application>"
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.example.android.apis"
+ android:label="Tests for Api Demos."/>
+</manifest> \ No newline at end of file
diff --git a/tools/scripts/android_rules.xml b/tools/scripts/android_rules.xml
index aad9dbd6e..003021c35 100644
--- a/tools/scripts/android_rules.xml
+++ b/tools/scripts/android_rules.xml
@@ -72,6 +72,7 @@
<!-- Create the output directories if they don't exist yet. -->
<target name="dirs">
<echo>Creating output directories if needed...</echo>
+ <mkdir dir="${resource-folder}" />
<mkdir dir="${external-libs-folder}" />
<mkdir dir="${gen-folder}" />
<mkdir dir="${out-folder}" />
diff --git a/tools/scripts/divide_and_compress.py b/tools/scripts/divide_and_compress.py
index d369be4a4..2bcb0ab67 100755
--- a/tools/scripts/divide_and_compress.py
+++ b/tools/scripts/divide_and_compress.py
@@ -36,89 +36,99 @@ from.
__author__ = 'jmatt@google.com (Justin Mattson)'
-from optparse import OptionParser
+import optparse
import os
import stat
import sys
import zipfile
-from zipfile import ZipFile
import divide_and_compress_constants
-def Main(argv):
- parser = CreateOptionsParser()
- (options, args) = parser.parse_args()
- VerifyArguments(options, parser)
- zipper = DirectoryZipper(options.destination,
- options.sourcefiles,
- ParseSize(options.filesize),
- options.compress)
- zipper.StartCompress()
-
-
def CreateOptionsParser():
- rtn = OptionParser()
+ """Creates the parser for command line arguments.
+
+ Returns:
+ A configured optparse.OptionParser object.
+ """
+ rtn = optparse.OptionParser()
rtn.add_option('-s', '--sourcefiles', dest='sourcefiles', default=None,
help='The directory containing the files to compress')
rtn.add_option('-d', '--destination', dest='destination', default=None,
help=('Where to put the archive files, this should not be'
' a child of where the source files exist.'))
rtn.add_option('-f', '--filesize', dest='filesize', default='1M',
- help=('Maximum size of archive files. A number followed by'
- 'a magnitude indicator, eg. 1000000B == one million '
- 'BYTES, 500K == five hundred KILOBYTES, 1.2M == one '
- 'point two MEGABYTES. 1M == 1048576 BYTES'))
+ help=('Maximum size of archive files. A number followed by '
+ 'a magnitude indicator either "B", "K", "M", or "G". '
+ 'Examples:\n 1000000B == one million BYTES\n'
+ ' 1.2M == one point two MEGABYTES\n'
+ ' 1M == 1048576 BYTES'))
rtn.add_option('-n', '--nocompress', action='store_false', dest='compress',
- default=True,
+ default=True,
help=('Whether the archive files should be compressed, or '
'just a concatenation of the source files'))
return rtn
def VerifyArguments(options, parser):
+ """Runs simple checks on correctness of commandline arguments.
+
+ Args:
+ options: The command line options passed.
+ parser: The parser object used to parse the command string.
+ """
try:
if options.sourcefiles is None or options.destination is None:
parser.print_help()
sys.exit(-1)
- except (AttributeError), err:
+ except AttributeError:
parser.print_help()
sys.exit(-1)
def ParseSize(size_str):
+ """Parse the file size argument from a string to a number of bytes.
+
+ Args:
+ size_str: The string representation of the file size.
+
+ Returns:
+ The file size in bytes.
+
+ Raises:
+ ValueError: Raises an error if the numeric or qualifier portions of the
+ file size argument is invalid.
+ """
if len(size_str) < 2:
raise ValueError(('filesize argument not understood, please include'
' a numeric value and magnitude indicator'))
- magnitude = size_str[len(size_str)-1:]
- if not magnitude in ('K', 'B', 'M'):
- raise ValueError(('filesize magnitude indicator not valid, must be \'K\','
- '\'B\', or \'M\''))
- numeral = float(size_str[0:len(size_str)-1])
+ magnitude = size_str[-1]
+ if not magnitude in ('B', 'K', 'M', 'G'):
+ raise ValueError(('filesize magnitude indicator not valid, must be "B",'
+ '"K","M", or "G"'))
+ numeral = float(size_str[:-1])
if magnitude == 'K':
numeral *= 1024
elif magnitude == 'M':
numeral *= 1048576
+ elif magnitude == 'G':
+ numeral *= 1073741824
return int(numeral)
class DirectoryZipper(object):
- """Class to compress a directory and all its sub-directories."""
- current_archive = None
- output_dir = None
- base_path = None
- max_size = None
- compress = None
- index_fp = None
+ """Class to compress a directory and all its sub-directories."""
def __init__(self, output_path, base_dir, archive_size, enable_compression):
"""DirectoryZipper constructor.
Args:
- output_path: the path to write the archives and index file to
- base_dir: the directory to compress
- archive_size: the maximum size, in bytes, of a single archive file
- enable_compression: whether or not compression should be enabled, if
- disabled, the files will be written into an uncompresed zip
+ output_path: A string, the path to write the archives and index file to.
+ base_dir: A string, the directory to compress.
+ archive_size: An number, the maximum size, in bytes, of a single
+ archive file.
+ enable_compression: A boolean, whether or not compression should be
+ enabled, if disabled, the files will be written into an uncompresed
+ zip.
"""
self.output_dir = output_path
self.current_archive = '0.zip'
@@ -126,6 +136,9 @@ class DirectoryZipper(object):
self.max_size = archive_size
self.compress = enable_compression
+ # Set index_fp to None, because we don't know what it will be yet.
+ self.index_fp = None
+
def StartCompress(self):
"""Start compress of the directory.
@@ -133,7 +146,7 @@ class DirectoryZipper(object):
specified output directory. It will also produce an 'index.txt' file in the
output directory that maps from file to archive.
"""
- self.index_fp = open(''.join([self.output_dir, 'main.py']), 'w')
+ self.index_fp = open(os.path.join(self.output_dir, 'main.py'), 'w')
self.index_fp.write(divide_and_compress_constants.file_preamble)
os.path.walk(self.base_path, self.CompressDirectory, 1)
self.index_fp.write(divide_and_compress_constants.file_endpiece)
@@ -149,37 +162,32 @@ class DirectoryZipper(object):
Args:
archive_path: Path to the archive to modify. This archive should not be
open elsewhere, since it will need to be deleted.
- Return:
- A new ZipFile object that points to the modified archive file
+
+ Returns:
+ A new ZipFile object that points to the modified archive file.
"""
if archive_path is None:
- archive_path = ''.join([self.output_dir, self.current_archive])
+ archive_path = os.path.join(self.output_dir, self.current_archive)
- # Move the old file and create a new one at its old location
- ext_offset = archive_path.rfind('.')
- old_archive = ''.join([archive_path[0:ext_offset], '-old',
- archive_path[ext_offset:]])
+ # Move the old file and create a new one at its old location.
+ root, ext = os.path.splitext(archive_path)
+ old_archive = ''.join([root, '-old', ext])
os.rename(archive_path, old_archive)
old_fp = self.OpenZipFileAtPath(old_archive, mode='r')
+ # By default, store uncompressed.
+ compress_bit = zipfile.ZIP_STORED
if self.compress:
- new_fp = self.OpenZipFileAtPath(archive_path,
- mode='w',
- compress=zipfile.ZIP_DEFLATED)
- else:
- new_fp = self.OpenZipFileAtPath(archive_path,
- mode='w',
- compress=zipfile.ZIP_STORED)
-
- # Read the old archive in a new archive, except the last one
- zip_members = enumerate(old_fp.infolist())
- num_members = len(old_fp.infolist())
- while num_members > 1:
- this_member = zip_members.next()[1]
- new_fp.writestr(this_member.filename, old_fp.read(this_member.filename))
- num_members -= 1
-
- # Close files and delete the old one
+ compress_bit = zipfile.ZIP_DEFLATED
+ new_fp = self.OpenZipFileAtPath(archive_path,
+ mode='w',
+ compress=compress_bit)
+
+ # Read the old archive in a new archive, except the last one.
+ for zip_member in old_fp.infolist()[:-1]:
+ new_fp.writestr(zip_member, old_fp.read(zip_member.filename))
+
+ # Close files and delete the old one.
old_fp.close()
new_fp.close()
os.unlink(old_archive)
@@ -193,11 +201,11 @@ class DirectoryZipper(object):
mode = 'w'
if mode == 'r':
- return ZipFile(path, mode)
+ return zipfile.ZipFile(path, mode)
else:
- return ZipFile(path, mode, compress)
+ return zipfile.ZipFile(path, mode, compress)
- def CompressDirectory(self, irrelevant, dir_path, dir_contents):
+ def CompressDirectory(self, unused_id, dir_path, dir_contents):
"""Method to compress the given directory.
This method compresses the directory 'dir_path'. It will add to an existing
@@ -206,40 +214,35 @@ class DirectoryZipper(object):
mapping of files to archives to the self.index_fp file descriptor
Args:
- irrelevant: a numeric identifier passed by the os.path.walk method, this
- is not used by this method
- dir_path: the path to the directory to compress
- dir_contents: a list of directory contents to be compressed
+ unused_id: A numeric identifier passed by the os.path.walk method, this
+ is not used by this method.
+ dir_path: A string, the path to the directory to compress.
+ dir_contents: A list of directory contents to be compressed.
"""
-
- # construct the queue of files to be added that this method will use
+ # Construct the queue of files to be added that this method will use
# it seems that dir_contents is given in reverse alphabetical order,
- # so put them in alphabetical order by inserting to front of the list
+ # so put them in alphabetical order by inserting to front of the list.
dir_contents.sort()
zip_queue = []
- if dir_path[len(dir_path) - 1:] == os.sep:
- for filename in dir_contents:
- zip_queue.append(''.join([dir_path, filename]))
- else:
- for filename in dir_contents:
- zip_queue.append(''.join([dir_path, os.sep, filename]))
+ for filename in dir_contents:
+ zip_queue.append(os.path.join(dir_path, filename))
compress_bit = zipfile.ZIP_DEFLATED
if not self.compress:
compress_bit = zipfile.ZIP_STORED
- # zip all files in this directory, adding to existing archives and creating
- # as necessary
- while len(zip_queue) > 0:
+ # Zip all files in this directory, adding to existing archives and creating
+ # as necessary.
+ while zip_queue:
target_file = zip_queue[0]
if os.path.isfile(target_file):
self.AddFileToArchive(target_file, compress_bit)
-
- # see if adding the new file made our archive too large
+
+ # See if adding the new file made our archive too large.
if not self.ArchiveIsValid():
-
+
# IF fixing fails, the last added file was to large, skip it
# ELSE the current archive filled normally, make a new one and try
- # adding the file again
+ # adding the file again.
if not self.FixArchive('SIZE'):
zip_queue.pop(0)
else:
@@ -248,7 +251,7 @@ class DirectoryZipper(object):
0:self.current_archive.rfind('.zip')]) + 1)
else:
- # if this the first file in the archive, write an index record
+ # Write an index record if necessary.
self.WriteIndexRecord()
zip_queue.pop(0)
else:
@@ -260,10 +263,10 @@ class DirectoryZipper(object):
Only write an index record if this is the first file to go into archive
Returns:
- True if an archive record is written, False if it isn't
+ True if an archive record is written, False if it isn't.
"""
archive = self.OpenZipFileAtPath(
- ''.join([self.output_dir, self.current_archive]), 'r')
+ os.path.join(self.output_dir, self.current_archive), 'r')
archive_index = archive.infolist()
if len(archive_index) == 1:
self.index_fp.write(
@@ -279,54 +282,56 @@ class DirectoryZipper(object):
"""Make the archive compliant.
Args:
- problem: the reason the archive is invalid
+ problem: An enum, the reason the archive is invalid.
Returns:
Whether the file(s) removed to fix the archive could conceivably be
in an archive, but for some reason can't be added to this one.
"""
- archive_path = ''.join([self.output_dir, self.current_archive])
- rtn_value = None
-
+ archive_path = os.path.join(self.output_dir, self.current_archive)
+ return_value = None
+
if problem == 'SIZE':
archive_obj = self.OpenZipFileAtPath(archive_path, mode='r')
num_archive_files = len(archive_obj.infolist())
-
+
# IF there is a single file, that means its too large to compress,
# delete the created archive
- # ELSE do normal finalization
+ # ELSE do normal finalization.
if num_archive_files == 1:
print ('WARNING: %s%s is too large to store.' % (
self.base_path, archive_obj.infolist()[0].filename))
archive_obj.close()
os.unlink(archive_path)
- rtn_value = False
+ return_value = False
else:
- self.RemoveLastFile(''.join([self.output_dir, self.current_archive]))
archive_obj.close()
+ self.RemoveLastFile(
+ os.path.join(self.output_dir, self.current_archive))
print 'Final archive size for %s is %i' % (
- self.current_archive, os.stat(archive_path)[stat.ST_SIZE])
- rtn_value = True
- return rtn_value
+ self.current_archive, os.path.getsize(archive_path))
+ return_value = True
+ return return_value
def AddFileToArchive(self, filepath, compress_bit):
"""Add the file at filepath to the current archive.
Args:
- filepath: the path of the file to add
- compress_bit: whether or not this fiel should be compressed when added
+ filepath: A string, the path of the file to add.
+ compress_bit: A boolean, whether or not this file should be compressed
+ when added.
Returns:
True if the file could be added (typically because this is a file) or
- False if it couldn't be added (typically because its a directory)
+ False if it couldn't be added (typically because its a directory).
"""
- curr_archive_path = ''.join([self.output_dir, self.current_archive])
- if os.path.isfile(filepath):
- if os.stat(filepath)[stat.ST_SIZE] > 1048576:
+ curr_archive_path = os.path.join(self.output_dir, self.current_archive)
+ if os.path.isfile(filepath) and not os.path.islink(filepath):
+ if os.path.getsize(filepath) > 1048576:
print 'Warning: %s is potentially too large to serve on GAE' % filepath
archive = self.OpenZipFileAtPath(curr_archive_path,
compress=compress_bit)
- # add the file to the archive
+ # Add the file to the archive.
archive.write(filepath, filepath[len(self.base_path):])
archive.close()
return True
@@ -340,13 +345,22 @@ class DirectoryZipper(object):
The thought is that eventually this will do additional validation
Returns:
- True if the archive is valid, False if its not
+ True if the archive is valid, False if its not.
"""
- archive_path = ''.join([self.output_dir, self.current_archive])
- if os.stat(archive_path)[stat.ST_SIZE] > self.max_size:
- return False
- else:
- return True
+ archive_path = os.path.join(self.output_dir, self.current_archive)
+ return os.path.getsize(archive_path) <= self.max_size
+
+
+def main(argv):
+ parser = CreateOptionsParser()
+ (options, unused_args) = parser.parse_args(args=argv[1:])
+ VerifyArguments(options, parser)
+ zipper = DirectoryZipper(options.destination,
+ options.sourcefiles,
+ ParseSize(options.filesize),
+ options.compress)
+ zipper.StartCompress()
+
if __name__ == '__main__':
- Main(sys.argv)
+ main(sys.argv)
diff --git a/tools/scripts/divide_and_compress_constants.py b/tools/scripts/divide_and_compress_constants.py
index 4e11b6fae..15162b712 100644
--- a/tools/scripts/divide_and_compress_constants.py
+++ b/tools/scripts/divide_and_compress_constants.py
@@ -19,42 +19,40 @@
__author__ = 'jmatt@google.com (Justin Mattson)'
-file_preamble = ('#!/usr/bin/env python\n'
- '#\n'
- '# Copyright 2008 Google Inc.\n'
- '#\n'
- '# Licensed under the Apache License, Version 2.0 (the'
- '\"License");\n'
- '# you may not use this file except in compliance with the '
- 'License.\n'
- '# You may obtain a copy of the License at\n'
- '#\n'
- '# http://www.apache.org/licenses/LICENSE-2.0\n'
- '#\n'
- '# Unless required by applicable law or agreed to in writing,'
- ' software\n'
- '# distributed under the License is distributed on an \"AS'
- 'IS\" BASIS,\n'
- '# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either '
- 'express or implied.\n'
- '# See the License for the specific language governing'
- ' permissions and\n'
- '# limitations under the License.\n'
- '#\n\n'
- 'import wsgiref.handlers\n'
- 'from google.appengine.ext import zipserve\n'
- 'from google.appengine.ext import webapp\n'
- 'import memcache_zipserve\n\n\n'
- 'class MainHandler(webapp.RequestHandler):\n\n'
- ' def get(self):\n'
- ' self.response.out.write(\'Hello world!\')\n\n'
- 'def main():\n'
- ' application = webapp.WSGIApplication([(\'/(.*)\','
- ' memcache_zipserve.create_handler([')
-
-file_endpiece = ('])),\n'
- '],\n'
- 'debug=False)\n'
- ' wsgiref.handlers.CGIHandler().run(application)\n\n'
- 'if __name__ == \'__main__\':\n'
- ' main()')
+file_preamble = """#!/usr/bin/env python
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+#
+# 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.
+#
+
+import wsgiref.handlers\n'
+from google.appengine.ext import zipserve\n'
+from google.appengine.ext import webapp\n'
+import memcache_zipserve\n\n\n'
+class MainHandler(webapp.RequestHandler):
+
+ def get(self):
+ self.response.out.write('Hello world!')
+
+def main():
+ application = webapp.WSGIApplication(['/(.*)',
+ memcache_zipserve.create_handler(["""
+
+file_endpiece = """])),
+],
+debug=False)
+ wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == __main__:
+ main()"""
diff --git a/tools/scripts/test_divide_and_compress.py b/tools/scripts/divide_and_compress_test.py
index d0d27b3d0..426449af6 100755
--- a/tools/scripts/test_divide_and_compress.py
+++ b/tools/scripts/divide_and_compress_test.py
@@ -17,7 +17,7 @@
"""Tests for divide_and_compress.py.
-TODO: Add tests for module methods.
+TODO(jmatt): Add tests for module methods.
"""
__author__ = 'jmatt@google.com (Justin Mattson)'
@@ -26,10 +26,9 @@ import os
import stat
import unittest
import zipfile
-from zipfile import ZipFile
import divide_and_compress
-from mox import mox
+import mox
class BagOfParts(object):
@@ -58,6 +57,10 @@ class ValidAndRemoveTests(unittest.TestCase):
'sdjfljkgsc n;iself')
self.files = {'file1': file1, 'file2': file2}
+ def tearDown(self):
+ """Remove any stubs we've created."""
+ self.my_mox.UnsetStubs()
+
def testArchiveIsValid(self):
"""Test the DirectoryZipper.ArchiveIsValid method.
@@ -119,7 +122,7 @@ class ValidAndRemoveTests(unittest.TestCase):
A configured mocked
"""
- source_zip = self.my_mox.CreateMock(ZipFile)
+ source_zip = self.my_mox.CreateMock(zipfile.ZipFile)
source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
source_zip.read(self.files['file1'].filename).AndReturn(
@@ -137,16 +140,12 @@ class ValidAndRemoveTests(unittest.TestCase):
A configured mocked
"""
- dest_zip = mox.MockObject(ZipFile)
+ dest_zip = mox.MockObject(zipfile.ZipFile)
dest_zip.writestr(self.files['file1'].filename,
self.files['file1'].contents)
dest_zip.close()
return dest_zip
- def tearDown(self):
- """Remove any stubs we've created."""
- self.my_mox.UnsetStubs()
-
class FixArchiveTests(unittest.TestCase):
"""Tests for the DirectoryZipper.FixArchive method."""
@@ -158,6 +157,10 @@ class FixArchiveTests(unittest.TestCase):
self.file1.filename = 'file1.txt'
self.file1.contents = 'This is a test file'
+ def tearDown(self):
+ """Unset any mocks that we've created."""
+ self.my_mox.UnsetStubs()
+
def _InitMultiFileData(self):
"""Create an array of mock file objects.
@@ -211,7 +214,7 @@ class FixArchiveTests(unittest.TestCase):
Returns:
A configured mock object
"""
- mock_zip = self.my_mox.CreateMock(ZipFile)
+ mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
mock_zip.infolist().AndReturn([self.file1])
mock_zip.infolist().AndReturn([self.file1])
mock_zip.close()
@@ -250,15 +253,11 @@ class FixArchiveTests(unittest.TestCase):
A configured mock object
"""
self._InitMultiFileData()
- mock_zip = self.my_mox.CreateMock(ZipFile)
+ mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
mock_zip.infolist().AndReturn(self.multi_file_dir)
mock_zip.close()
return mock_zip
- def tearDown(self):
- """Unset any mocks that we've created."""
- self.my_mox.UnsetStubs()
-
class AddFileToArchiveTest(unittest.TestCase):
"""Test behavior of method to add a file to an archive."""
@@ -270,6 +269,9 @@ class AddFileToArchiveTest(unittest.TestCase):
self.file_to_add = 'file.txt'
self.input_dir = '/foo/bar/baz/'
+ def tearDown(self):
+ self.my_mox.UnsetStubs()
+
def testAddFileToArchive(self):
"""Test the DirectoryZipper.AddFileToArchive method.
@@ -312,15 +314,12 @@ class AddFileToArchiveTest(unittest.TestCase):
Returns:
A configured mock object
"""
- archive_mock = self.my_mox.CreateMock(ZipFile)
+ archive_mock = self.my_mox.CreateMock(zipfile.ZipFile)
archive_mock.write(''.join([self.input_dir, self.file_to_add]),
self.file_to_add)
archive_mock.close()
return archive_mock
- def tearDown(self):
- self.my_mox.UnsetStubs()
-
class CompressDirectoryTest(unittest.TestCase):
"""Test the master method of the class.
diff --git a/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java b/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java
index 154788ee1..adf37ed0b 100644
--- a/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java
+++ b/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java
@@ -35,6 +35,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
/**
* Main class for the 'android' application.
@@ -50,6 +51,12 @@ class Main {
private final static String[] BOOLEAN_YES_REPLIES = new String[] { "yes", "y" };
private final static String[] BOOLEAN_NO_REPLIES = new String[] { "no", "n" };
+ /** Regex used to validate characters that compose an AVD name. */
+ private final static Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+");
+ /** List of valid characters for an AVD name. Used for display purposes. */
+ private final static String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -";
+
+
/** Path to the SDK folder. This is the parent of {@link #TOOLSDIR}. */
private String mSdkFolder;
/** Logger object. Use this to print normal output, warnings or errors. */
@@ -239,11 +246,41 @@ class Main {
mSdkLog);
String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath());
+
+ String projectName = mSdkCommandLine.getParamName();
+ String packageName = mSdkCommandLine.getParamProjectPackage();
+ String activityName = mSdkCommandLine.getParamProjectActivity();
+ if (projectName != null &&
+ !ProjectCreator.RE_PROJECT_NAME.matcher(projectName).matches()) {
+ errorAndExit(
+ "Project name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
+ projectName, ProjectCreator.CHARS_PROJECT_NAME);
+ return;
+ }
+
+ if (activityName != null &&
+ !ProjectCreator.RE_ACTIVITY_NAME.matcher(activityName).matches()) {
+ errorAndExit(
+ "Activity name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
+ activityName, ProjectCreator.CHARS_ACTIVITY_NAME);
+ return;
+ }
+
+ if (packageName != null &&
+ !ProjectCreator.RE_PACKAGE_NAME.matcher(packageName).matches()) {
+ errorAndExit(
+ "Package name '%1$s' contains invalid characters.\n" +
+ "A package name must be constitued of two Java identifiers.\n" +
+ "Each identifier allowed characters are: %2$s",
+ packageName, ProjectCreator.CHARS_PACKAGE_NAME);
+ return;
+ }
+
creator.createProject(projectDir,
- mSdkCommandLine.getParamName(),
- mSdkCommandLine.getParamProjectPackage(),
- mSdkCommandLine.getParamProjectActivity(),
+ projectName,
+ activityName,
+ packageName,
target,
false /* isTestProject*/);
}
@@ -447,6 +484,14 @@ class Main {
AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog);
String avdName = mSdkCommandLine.getParamName();
+
+ if (!RE_AVD_NAME.matcher(avdName).matches()) {
+ errorAndExit(
+ "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
+ avdName, CHARS_AVD_NAME);
+ return;
+ }
+
AvdInfo info = avdManager.getAvd(avdName);
if (info != null) {
if (mSdkCommandLine.getFlagForce()) {
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
index a3da70e63..5efd55376 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java
@@ -28,7 +28,7 @@ final class PlatformTarget implements IAndroidTarget {
/** String used to get a hash to the platform target */
private final static String PLATFORM_HASH = "android-%d";
- private final static String PLATFORM_VENDOR = "Android";
+ private final static String PLATFORM_VENDOR = "Android Open Source Project";
private final static String PLATFORM_NAME = "Android %s";
private final String mLocation;
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java
index 65cbbe356..93577e42b 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java
+++ b/tools/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<AvdInfo> list = new ArrayList<AvdInfo>();
+ 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<AvdInfo> 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());
}
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java
index 7489b65d6..b84be18da 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java
@@ -62,6 +62,27 @@ public class ProjectCreator {
private final static String FOLDER_TESTS = "tests";
+ /** Pattern for characters accepted in a project name. Since this will be used as a
+ * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */
+ public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+");
+ /** List of valid characters for a project name. Used for display purposes. */
+ public final static String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _";
+
+ /** Pattern for characters accepted in a package name. A package is list of Java identifier
+ * separated by a dot. We need to have at least one dot (e.g. a two-level package name).
+ * A Java identifier cannot start by a digit. */
+ public static final Pattern RE_PACKAGE_NAME =
+ Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+");
+ /** List of valid characters for a project name. Used for display purposes. */
+ public final static String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _";
+
+ /** Pattern for characters accepted in an activity name, which is a Java identifier. */
+ public static final Pattern RE_ACTIVITY_NAME =
+ Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
+ /** List of valid characters for a project name. Used for display purposes. */
+ public final static String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _";
+
+
public enum OutputLevel {
/** Silent mode. Project creation will only display errors. */
SILENT,
@@ -106,11 +127,17 @@ public class ProjectCreator {
/**
* Creates a new project.
+ * <p/>
+ * The caller should have already checked and sanitized the parameters.
*
* @param folderPath the folder of the project to create.
- * @param projectName the name of the project.
- * @param packageName the package of the project.
- * @param activityName the activity of the project as it will appear in the manifest.
+ * @param projectName the name of the project. The name must match the
+ * {@link #RE_PROJECT_NAME} regex.
+ * @param packageName the package of the project. The name must match the
+ * {@link #RE_PACKAGE_NAME} regex.
+ * @param activityName the activity of the project as it will appear in the manifest. Can be
+ * null if no activity should be created. The name must match the
+ * {@link #RE_ACTIVITY_NAME} regex.
* @param target the project target.
* @param isTestProject whether the project to create is a test project.
*/