diff options
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 Binary files differnew file mode 100644 index 000000000..25826dce7 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/androidjunit.png 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. */ |
