├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── logo.png ├── pom.xml └── robotium-solo ├── pom.xml └── src └── main └── java └── com └── robotium └── solo ├── ActivityUtils.java ├── Asserter.java ├── By.java ├── Checker.java ├── Clicker.java ├── Condition.java ├── DialogUtils.java ├── GLRenderWrapper.java ├── Getter.java ├── Illustration.java ├── Illustrator.java ├── Presser.java ├── PressurePoint.java ├── Reflect.java ├── RobotiumTextView.java ├── RobotiumUtils.java ├── RobotiumWeb.js ├── RobotiumWebClient.java ├── Rotator.java ├── ScreenshotTaker.java ├── Scroller.java ├── Searcher.java ├── Sender.java ├── Setter.java ├── Sleeper.java ├── Solo.java ├── Swiper.java ├── SystemUtils.java ├── Tapper.java ├── TextEnterer.java ├── Timeout.java ├── ViewFetcher.java ├── ViewLocationComparator.java ├── Waiter.java ├── WebElement.java ├── WebElementCreator.java ├── WebUtils.java └── Zoomer.java /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target 3 | .idea 4 | *.iws 5 | *.ipr 6 | *.iml 7 | .classpath 8 | .project 9 | .settings 10 | .springBeans 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2012] [Robotium Developers] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | == NOTICE file corresponding to the section 4 d of == 3 | == the Apache License, Version 2.0, == 4 | == in this case for the Robotium-specific code. == 5 | ========================================================================= 6 | 7 | Robotium 8 | Copyright 2010, 2011 Jayway AB 9 | 10 | This product includes software developed at 11 | Jayway AB (http://www.jayway.com/). 12 | 13 | 14 | ========================================================================= 15 | == NOTICE file corresponding to the section 4 d of == 16 | == the Apache License, Version 2.0, == 17 | == in this case for the Android-specific code. == 18 | ========================================================================= 19 | 20 | Android Code 21 | Copyright 2005-2008 The Android Open Source Project 22 | 23 | This product includes software developed as part of 24 | The Android Open Source Project (http://source.android.com). 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Robotium](logo.png) 2 | 3 | # User scenario testing for Android 4 | Robotium is an Android test automation framework that has full support for native and hybrid applications. Robotium makes it easy to write powerful and robust automatic black-box UI tests for Android applications. With the support of Robotium, test case developers can write function, system and user acceptance test scenarios, spanning multiple Android activities. 5 | 6 | 7 | See [Questions & Answers](https://github.com/RobotiumTech/robotium/wiki/Questions-&-Answers) for common Robotium questions and answers. 8 |
9 | 10 | See [Getting Started](https://github.com/RobotiumTech/robotium/wiki/Getting-Started) for instructions and examples on how to create your first Robotium tests. 11 |
12 | 13 | Join the discussions in the [Robotium Developers Group](http://groups.google.com/group/robotium-developers). 14 | 15 | ---- 16 | ### NEWS: Robotium 5.6.3 is released! 17 | [Robotium 5.6.3](https://github.com/RobotiumTech/robotium/wiki/Downloads) is the fastest, most accurate and stable version of Robotium yet. 18 | 19 | New functionality includes: clickInRecyclerView(int itemIndex, int recyclerViewIndex, int id) and Config.sleepDuration, Config.sleepMiniDuration (to adjust the execution speed, lower means faster) and more. See the [Javadoc](http://recorder.robotium.com/javadoc/) for more information. 20 | 21 | ---- 22 | 23 | #### Robotium provides the following benefits: 24 | * Test Android apps, both native and hybrid. 25 | * Requires minimal knowledge of the application under test. 26 | * The framework handles multiple Android activities automatically. 27 | * Minimal time needed to write solid test cases. 28 | * Readability of test cases is greatly improved, compared to standard instrumentation tests. 29 | * Test cases are more robust due to the run-time binding to UI components. 30 | * Fast test case execution. 31 | * Integrates smoothly with Maven, Gradle or Ant to run tests as part of continuous integration. 32 | 33 | 34 | #### Robotium Recorder #### 35 | * Check out [Robotium Recorder](http://Robotium.com) that will allow you to record Robotium test cases in minutes. Now also available for Android Studio! 36 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotiumTech/robotium/75e567c38f26a6a87dc8bef90b3886a20e28d291/logo.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.jayway.android.robotium 4 | robotium 5 | 5.6.4-SNAPSHOT 6 | pom 7 | Robotium 8 | User scenario testing for Android 9 | http://www.robotium.org/ 10 | 2009 11 | 12 | robotium-solo 13 | 14 | 15 | master 16 | UTF-8 17 | 3.2.5 18 | 4.0.1.2 19 | 20 | 21 | http://github.com/jayway/robotium/tree/${scm.branch} 22 | scm:git:git://github.com/jayway/robotium.git 23 | scm:git:ssh://git@github.com/jayway/robotium.git 24 | HEAD 25 | 26 | 27 | Google Code Issue Tracking 28 | http://code.google.com/p/robotium/issues/list 29 | 30 | 31 | Hudson 32 | http://hudson.josefson.org/view/Robotium/ 33 | 34 | 35 | 36 | Apache 2.0 37 | LICENSE 38 | 39 | 40 | 41 | 42 | See homepage and mailinglist for contributors. 43 | robotium-project 44 | http://www.robotium.org/ 45 | 46 | Developer 47 | Contributor 48 | 49 | 50 | 51 | 52 | 53 | Robotium Developers 54 | http://groups.google.com/group/robotium-developers/topics 55 | 56 | 57 | 58 | ${maven.version} 59 | 60 | 61 | 62 | 63 | com.google.android 64 | android 65 | ${android.jar.version} 66 | provided 67 | 68 | 69 | com.google.android 70 | android-test 71 | ${android.jar.version} 72 | provided 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | maven-clean-plugin 81 | 2.6.1 82 | 83 | 84 | maven-compiler-plugin 85 | 3.2 86 | 87 | 88 | maven-deploy-plugin 89 | 2.8.2 90 | 91 | 92 | maven-install-plugin 93 | 2.5.2 94 | 95 | 96 | maven-gpg-plugin 97 | 1.6 98 | 99 | 100 | maven-jar-plugin 101 | 2.5 102 | 103 | 104 | maven-resources-plugin 105 | 2.7 106 | 107 | 108 | maven-surefire-plugin 109 | 2.18.1 110 | 111 | 112 | maven-javadoc-plugin 113 | 2.10.1 114 | 115 | 116 | maven-release-plugin 117 | 2.5.1 118 | 119 | 120 | maven-scm-plugin 121 | 1.9.2 122 | 123 | 124 | maven-site-plugin 125 | 3.4 126 | 127 | 128 | maven-source-plugin 129 | 2.4 130 | 131 | 132 | maven-enforcer-plugin 133 | 1.4 134 | 135 | 136 | 140 | org.codehaus.mojo 141 | versions-maven-plugin 142 | 2.1 143 | 144 | 145 | maven-assembly-plugin 146 | 2.5.3 147 | 148 | 149 | 150 | 151 | 152 | maven-scm-plugin 153 | 154 | branch 155 | ${scm.branch} 156 | 157 | 158 | 159 | maven-release-plugin 160 | 161 | true 162 | false 163 | -Pofficial-release 164 | 165 | 166 | 167 | maven-compiler-plugin 168 | 169 | 1.5 170 | 1.5 171 | 172 | 173 | 174 | maven-jar-plugin 175 | 176 | 177 | 178 | 1.5 179 | 1.5 180 | 181 | 182 | 183 | 184 | 185 | maven-enforcer-plugin 186 | 187 | 188 | enforce-maven 189 | 190 | enforce 191 | 192 | initialize 193 | 194 | 195 | 196 | 197 | 198 | [${maven.version},) 199 | Check for Maven version >=${maven.version} failed. Update your Maven install. 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | src/main/resources 208 | 209 | 210 | src/main/java 211 | 212 | **/*.js 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | oss.sonatype.org-jayway-staging 221 | OpenSource Release Staging on Sonatype.org 222 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 223 | 224 | 225 | oss.sonatype.org-jayway-snapshots 226 | Jayway OpenSource SNAPSHOTs on Sonatype.org 227 | https://oss.sonatype.org/content/repositories/jayway-snapshots/ 228 | 229 | 230 | 231 | 232 | 233 | official-release 234 | 235 | 236 | 237 | 238 | maven-gpg-plugin 239 | 240 | true 241 | 242 | 243 | 244 | sign-artifacts 245 | verify 246 | 247 | sign 248 | 249 | 250 | 251 | 252 | 253 | 254 | true 255 | maven-deploy-plugin 256 | 257 | true 258 | 259 | 260 | 261 | maven-source-plugin 262 | 263 | 264 | attach-sources 265 | 266 | jar-no-fork 267 | 268 | 269 | 270 | 271 | 272 | maven-javadoc-plugin 273 | 274 | ${project.build.sourceEncoding} 275 | 276 | 277 | 278 | attach-javadocs 279 | 280 | jar 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /robotium-solo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.jayway.android.robotium 4 | robotium-solo 5 | 5.6.4-SNAPSHOT 6 | jar 7 | Robotium :: Solo 8 | 9 | com.jayway.android.robotium 10 | robotium 11 | 5.6.4-SNAPSHOT 12 | 13 | 14 | 15 | com.google.android 16 | android 17 | 18 | 19 | com.google.android 20 | android-test 21 | 22 | 23 | com.google.android 24 | support-v4 25 | r7 26 | provided 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/ActivityUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.Stack; 7 | import java.util.Timer; 8 | import com.robotium.solo.Solo.Config; 9 | import junit.framework.Assert; 10 | import android.app.Activity; 11 | import android.app.Instrumentation; 12 | import android.app.Instrumentation.ActivityMonitor; 13 | import android.content.IntentFilter; 14 | import android.os.SystemClock; 15 | import android.util.Log; 16 | import android.view.KeyEvent; 17 | 18 | /** 19 | * Contains activity related methods. Examples are: 20 | * getCurrentActivity(), getActivityMonitor(), setActivityOrientation(int orientation). 21 | * 22 | * @author Renas Reda, renas.reda@robotium.com 23 | * 24 | */ 25 | 26 | class ActivityUtils { 27 | 28 | private final Config config; 29 | private final Instrumentation inst; 30 | private ActivityMonitor activityMonitor; 31 | private Activity activity; 32 | private final Sleeper sleeper; 33 | private final String LOG_TAG = "Robotium"; 34 | private final int MINISLEEP = 100; 35 | private Stack> activityStack; 36 | private WeakReference weakActivityReference; 37 | private Stack activitiesStoredInActivityStack; 38 | private Timer activitySyncTimer; 39 | private volatile boolean registerActivities; 40 | Thread activityThread; 41 | 42 | /** 43 | * Constructs this object. 44 | * 45 | * @param config the {@code Config} instance 46 | * @param inst the {@code Instrumentation} instance. 47 | * @param activity the start {@code Activity} 48 | * @param sleeper the {@code Sleeper} instance 49 | */ 50 | 51 | public ActivityUtils(Config config, Instrumentation inst, Activity activity, Sleeper sleeper) { 52 | this.config = config; 53 | this.inst = inst; 54 | this.activity = activity; 55 | this.sleeper = sleeper; 56 | createStackAndPushStartActivity(); 57 | activitySyncTimer = new Timer(); 58 | activitiesStoredInActivityStack = new Stack(); 59 | setupActivityMonitor(); 60 | setupActivityStackListener(); 61 | } 62 | 63 | 64 | 65 | /** 66 | * Creates a new activity stack and pushes the start activity. 67 | */ 68 | 69 | private void createStackAndPushStartActivity(){ 70 | activityStack = new Stack>(); 71 | if (activity != null && config.trackActivities){ 72 | WeakReference weakReference = new WeakReference(activity); 73 | activity = null; 74 | activityStack.push(weakReference); 75 | } 76 | } 77 | 78 | 79 | /** 80 | * Returns a {@code List} of all the opened/active activities. 81 | * 82 | * @return a {@code List} of all the opened/active activities 83 | */ 84 | 85 | public ArrayList getAllOpenedActivities() 86 | { 87 | ArrayList activities = new ArrayList(); 88 | Iterator> activityStackIterator = activityStack.iterator(); 89 | 90 | while(activityStackIterator.hasNext()){ 91 | Activity activity = activityStackIterator.next().get(); 92 | if(activity!=null) 93 | activities.add(activity); 94 | } 95 | return activities; 96 | } 97 | 98 | /** 99 | * This is were the activityMonitor is set up. The monitor will keep check 100 | * for the currently active activity. 101 | */ 102 | 103 | private void setupActivityMonitor() { 104 | if(config.trackActivities){ 105 | try { 106 | IntentFilter filter = null; 107 | activityMonitor = inst.addMonitor(filter, null, false); 108 | } catch (Exception e) { 109 | e.printStackTrace(); 110 | } 111 | } 112 | } 113 | 114 | 115 | /** 116 | * Returns true if registration of Activites should be performed 117 | * 118 | * @return true if registration of Activities should be performed 119 | */ 120 | 121 | public boolean shouldRegisterActivities() { 122 | return registerActivities; 123 | } 124 | 125 | 126 | /** 127 | * Set true if registration of Activities should be performed 128 | * @param registerActivities true if registration of Activities should be performed 129 | * 130 | */ 131 | 132 | public void setRegisterActivities(boolean registerActivities) { 133 | this.registerActivities = registerActivities; 134 | } 135 | 136 | /** 137 | * This is were the activityStack listener is set up. The listener will keep track of the 138 | * opened activities and their positions. 139 | */ 140 | 141 | private void setupActivityStackListener() { 142 | if(activityMonitor == null){ 143 | return; 144 | } 145 | 146 | setRegisterActivities(true); 147 | 148 | activityThread = new RegisterActivitiesThread(this); 149 | activityThread.start(); 150 | } 151 | 152 | 153 | void monitorActivities() { 154 | if(activityMonitor != null){ 155 | Activity activity = activityMonitor.waitForActivityWithTimeout(2000L); 156 | 157 | if(activity != null){ 158 | if (activitiesStoredInActivityStack.remove(activity.toString())){ 159 | removeActivityFromStack(activity); 160 | } 161 | if(!activity.isFinishing()){ 162 | addActivityToStack(activity); 163 | } 164 | } 165 | } 166 | } 167 | 168 | 169 | 170 | /** 171 | * Removes a given activity from the activity stack 172 | * 173 | * @param activity the activity to remove 174 | */ 175 | 176 | private void removeActivityFromStack(Activity activity){ 177 | 178 | Iterator> activityStackIterator = activityStack.iterator(); 179 | while(activityStackIterator.hasNext()){ 180 | Activity activityFromWeakReference = activityStackIterator.next().get(); 181 | 182 | if(activityFromWeakReference == null){ 183 | activityStackIterator.remove(); 184 | } 185 | 186 | if(activity != null && activityFromWeakReference != null && activityFromWeakReference.equals(activity)){ 187 | activityStackIterator.remove(); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Returns the ActivityMonitor used by Robotium. 194 | * 195 | * @return the ActivityMonitor used by Robotium 196 | */ 197 | 198 | public ActivityMonitor getActivityMonitor(){ 199 | return activityMonitor; 200 | } 201 | 202 | /** 203 | * Sets the Orientation (Landscape/Portrait) for the current activity. 204 | * 205 | * @param orientation An orientation constant such as {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} or {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} 206 | */ 207 | 208 | public void setActivityOrientation(int orientation) 209 | { 210 | Activity activity = getCurrentActivity(); 211 | if(activity != null){ 212 | activity.setRequestedOrientation(orientation); 213 | } 214 | } 215 | 216 | /** 217 | * Returns the current {@code Activity}, after sleeping a default pause length. 218 | * 219 | * @param shouldSleepFirst whether to sleep a default pause first 220 | * @return the current {@code Activity} 221 | */ 222 | 223 | public Activity getCurrentActivity(boolean shouldSleepFirst) { 224 | return getCurrentActivity(shouldSleepFirst, true); 225 | } 226 | 227 | /** 228 | * Returns the current {@code Activity}, after sleeping a default pause length. 229 | * 230 | * @return the current {@code Activity} 231 | */ 232 | 233 | public Activity getCurrentActivity() { 234 | return getCurrentActivity(true, true); 235 | } 236 | 237 | /** 238 | * Adds an activity to the stack 239 | * 240 | * @param activity the activity to add 241 | */ 242 | 243 | private void addActivityToStack(Activity activity){ 244 | activitiesStoredInActivityStack.push(activity.toString()); 245 | weakActivityReference = new WeakReference(activity); 246 | activity = null; 247 | activityStack.push(weakActivityReference); 248 | } 249 | 250 | /** 251 | * Waits for an activity to be started if one is not provided 252 | * by the constructor. 253 | */ 254 | 255 | private final void waitForActivityIfNotAvailable(){ 256 | if(activityStack.isEmpty() || activityStack.peek().get() == null){ 257 | 258 | if (activityMonitor != null) { 259 | Activity activity = activityMonitor.getLastActivity(); 260 | while (activity == null){ 261 | sleeper.sleepMini(); 262 | activity = activityMonitor.getLastActivity(); 263 | } 264 | addActivityToStack(activity); 265 | } 266 | else if(config.trackActivities){ 267 | sleeper.sleepMini(); 268 | setupActivityMonitor(); 269 | waitForActivityIfNotAvailable(); 270 | } 271 | } 272 | } 273 | 274 | /** 275 | * Returns the name of the most recent Activity 276 | * 277 | * @return the name of the current {@code Activity} 278 | */ 279 | 280 | public String getCurrentActivityName(){ 281 | if(!activitiesStoredInActivityStack.isEmpty()){ 282 | return activitiesStoredInActivityStack.peek(); 283 | } 284 | return ""; 285 | } 286 | 287 | /** 288 | * Returns the current {@code Activity}. 289 | * 290 | * @param shouldSleepFirst whether to sleep a default pause first 291 | * @param waitForActivity whether to wait for the activity 292 | * @return the current {@code Activity} 293 | */ 294 | 295 | public Activity getCurrentActivity(boolean shouldSleepFirst, boolean waitForActivity) { 296 | if(shouldSleepFirst){ 297 | sleeper.sleep(); 298 | } 299 | if(!config.trackActivities){ 300 | return activity; 301 | } 302 | 303 | if(waitForActivity){ 304 | waitForActivityIfNotAvailable(); 305 | } 306 | if(!activityStack.isEmpty()){ 307 | activity=activityStack.peek().get(); 308 | } 309 | return activity; 310 | } 311 | 312 | /** 313 | * Check if activity stack is empty. 314 | * 315 | * @return true if activity stack is empty 316 | */ 317 | 318 | public boolean isActivityStackEmpty() { 319 | return activityStack.isEmpty(); 320 | } 321 | 322 | /** 323 | * Returns to the given {@link Activity}. 324 | * 325 | * @param name the name of the {@code Activity} to return to, e.g. {@code "MyActivity"} 326 | */ 327 | 328 | public void goBackToActivity(String name) 329 | { 330 | ArrayList activitiesOpened = getAllOpenedActivities(); 331 | boolean found = false; 332 | for(int i = 0; i < activitiesOpened.size(); i++){ 333 | if(activitiesOpened.get(i).getClass().getSimpleName().equals(name)){ 334 | found = true; 335 | break; 336 | } 337 | } 338 | if(found){ 339 | while(!getCurrentActivity().getClass().getSimpleName().equals(name)) 340 | { 341 | try{ 342 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 343 | }catch(SecurityException ignored){} 344 | } 345 | } 346 | else{ 347 | for (int i = 0; i < activitiesOpened.size(); i++){ 348 | Log.d(LOG_TAG, "Activity priorly opened: "+ activitiesOpened.get(i).getClass().getSimpleName()); 349 | } 350 | Assert.fail("No Activity named: '" + name + "' has been priorly opened"); 351 | } 352 | } 353 | 354 | /** 355 | * Returns a localized string. 356 | * 357 | * @param resId the resource ID for the string 358 | * @return the localized string 359 | */ 360 | 361 | public String getString(int resId) 362 | { 363 | Activity activity = getCurrentActivity(false); 364 | if(activity == null){ 365 | return ""; 366 | } 367 | return activity.getString(resId); 368 | } 369 | 370 | /** 371 | * Finalizes the solo object. 372 | */ 373 | 374 | @Override 375 | public void finalize() throws Throwable { 376 | activitySyncTimer.cancel(); 377 | stopActivityMonitor(); 378 | super.finalize(); 379 | } 380 | 381 | /** 382 | * Removes the ActivityMonitor 383 | */ 384 | private void stopActivityMonitor(){ 385 | try { 386 | // Remove the monitor added during startup 387 | if (activityMonitor != null) { 388 | inst.removeMonitor(activityMonitor); 389 | activityMonitor = null; 390 | } 391 | } catch (Exception ignored) {} 392 | 393 | } 394 | 395 | /** 396 | * All activites that have been opened are finished. 397 | */ 398 | 399 | public void finishOpenedActivities(){ 400 | // Stops the activityStack listener 401 | activitySyncTimer.cancel(); 402 | if(!config.trackActivities){ 403 | useGoBack(3); 404 | return; 405 | } 406 | ArrayList activitiesOpened = getAllOpenedActivities(); 407 | // Finish all opened activities 408 | for (int i = activitiesOpened.size()-1; i >= 0; i--) { 409 | sleeper.sleep(MINISLEEP); 410 | finishActivity(activitiesOpened.get(i)); 411 | } 412 | activitiesOpened = null; 413 | sleeper.sleep(MINISLEEP); 414 | // Finish the initial activity, pressing Back for good measure 415 | finishActivity(getCurrentActivity(true, false)); 416 | stopActivityMonitor(); 417 | setRegisterActivities(false); 418 | this.activity = null; 419 | sleeper.sleepMini(); 420 | useGoBack(1); 421 | clearActivityStack(); 422 | } 423 | 424 | /** 425 | * Sends the back button command a given number of times 426 | * 427 | * @param numberOfTimes the number of times to press "back" 428 | */ 429 | 430 | private void useGoBack(int numberOfTimes){ 431 | for(int i = 0; i < numberOfTimes; i++){ 432 | try { 433 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 434 | sleeper.sleep(MINISLEEP); 435 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 436 | } catch (Throwable ignored) { 437 | // Guard against lack of INJECT_EVENT permission 438 | } 439 | } 440 | } 441 | 442 | /** 443 | * Clears the activity stack. 444 | */ 445 | 446 | private void clearActivityStack(){ 447 | 448 | activityStack.clear(); 449 | activitiesStoredInActivityStack.clear(); 450 | } 451 | 452 | /** 453 | * Finishes an activity. 454 | * 455 | * @param activity the activity to finish 456 | */ 457 | 458 | private void finishActivity(Activity activity){ 459 | if(activity != null) { 460 | try{ 461 | activity.finish(); 462 | }catch(Throwable e){ 463 | e.printStackTrace(); 464 | } 465 | } 466 | } 467 | 468 | private static final class RegisterActivitiesThread extends Thread { 469 | 470 | public static final long REGISTER_ACTIVITY_THREAD_SLEEP_MS = 16L; 471 | private final WeakReference activityUtilsWR; 472 | 473 | RegisterActivitiesThread(ActivityUtils activityUtils) { 474 | super("activityMonitorThread"); 475 | activityUtilsWR = new WeakReference(activityUtils); 476 | setPriority(Thread.MIN_PRIORITY); 477 | } 478 | 479 | @Override 480 | public void run() { 481 | while (shouldMonitor()) { 482 | monitorActivities(); 483 | SystemClock.sleep(REGISTER_ACTIVITY_THREAD_SLEEP_MS); 484 | } 485 | } 486 | 487 | private boolean shouldMonitor() { 488 | ActivityUtils activityUtils = activityUtilsWR.get(); 489 | 490 | return activityUtils != null && activityUtils.shouldRegisterActivities(); 491 | } 492 | 493 | private void monitorActivities() { 494 | ActivityUtils activityUtils = activityUtilsWR.get(); 495 | if (activityUtils != null) { 496 | activityUtils.monitorActivities(); 497 | } 498 | } 499 | } 500 | 501 | } 502 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Asserter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Activity; 5 | import android.app.ActivityManager; 6 | 7 | /** 8 | * Contains assert methods examples are assertActivity() and assertLowMemory(). 9 | * 10 | * @author Renas Reda, renas.reda@robotium.com 11 | * 12 | */ 13 | 14 | class Asserter { 15 | private final ActivityUtils activityUtils; 16 | private final Waiter waiter; 17 | 18 | /** 19 | * Constructs this object. 20 | * 21 | * @param activityUtils the {@code ActivityUtils} instance. 22 | * @param waiter the {@code Waiter} instance. 23 | */ 24 | 25 | public Asserter(ActivityUtils activityUtils, Waiter waiter) { 26 | this.activityUtils = activityUtils; 27 | this.waiter = waiter; 28 | } 29 | 30 | /** 31 | * Asserts that an expected {@link Activity} is currently active one. 32 | * 33 | * @param message the message that should be displayed if the assert fails 34 | * @param name the name of the {@code Activity} that is expected to be active e.g. {@code "MyActivity"} 35 | */ 36 | 37 | public void assertCurrentActivity(String message, String name) { 38 | boolean foundActivity = waiter.waitForActivity(name); 39 | 40 | if(!foundActivity){ 41 | Activity activity = activityUtils.getCurrentActivity(); 42 | if(activity != null){ 43 | Assert.assertEquals(message, name, activity.getClass().getSimpleName()); 44 | } 45 | else{ 46 | Assert.assertEquals(message, name, "No actvity found"); 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Asserts that an expected {@link Activity} is currently active one. 53 | * 54 | * @param message the message that should be displayed if the assert fails 55 | * @param expectedClass the {@code Class} object that is expected to be active e.g. {@code MyActivity.class} 56 | */ 57 | 58 | public void assertCurrentActivity(String message, Class expectedClass) { 59 | if(expectedClass == null){ 60 | Assert.fail("The specified Activity is null!"); 61 | } 62 | 63 | boolean foundActivity = waiter.waitForActivity(expectedClass); 64 | 65 | if(!foundActivity) { 66 | Activity activity = activityUtils.getCurrentActivity(); 67 | if(activity != null){ 68 | Assert.assertEquals(message, expectedClass.getName(), activity.getClass().getName()); 69 | } 70 | else{ 71 | Assert.assertEquals(message, expectedClass.getName(), "No activity found"); 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Asserts that an expected {@link Activity} is currently active one, with the possibility to 78 | * verify that the expected {@code Activity} is a new instance of the {@code Activity}. 79 | * 80 | * @param message the message that should be displayed if the assert fails 81 | * @param name the name of the {@code Activity} that is expected to be active e.g. {@code "MyActivity"} 82 | * @param isNewInstance {@code true} if the expected {@code Activity} is a new instance of the {@code Activity} 83 | */ 84 | 85 | public void assertCurrentActivity(String message, String name, boolean isNewInstance) { 86 | assertCurrentActivity(message, name); 87 | Activity activity = activityUtils.getCurrentActivity(); 88 | if(activity != null){ 89 | assertCurrentActivity(message, activity.getClass(), 90 | isNewInstance); 91 | } 92 | } 93 | 94 | /** 95 | * Asserts that an expected {@link Activity} is currently active one, with the possibility to 96 | * verify that the expected {@code Activity} is a new instance of the {@code Activity}. 97 | * 98 | * @param message the message that should be displayed if the assert fails 99 | * @param expectedClass the {@code Class} object that is expected to be active e.g. {@code MyActivity.class} 100 | * @param isNewInstance {@code true} if the expected {@code Activity} is a new instance of the {@code Activity} 101 | */ 102 | 103 | public void assertCurrentActivity(String message, Class expectedClass, 104 | boolean isNewInstance) { 105 | boolean found = false; 106 | assertCurrentActivity(message, expectedClass); 107 | Activity activity = activityUtils.getCurrentActivity(false); 108 | if(activity == null){ 109 | Assert.assertNotSame(message, isNewInstance, false); 110 | return; 111 | } 112 | 113 | for (int i = 0; i < activityUtils.getAllOpenedActivities().size() - 1; i++) { 114 | String instanceString = activityUtils.getAllOpenedActivities().get(i).toString(); 115 | if (instanceString.equals(activity.toString())) 116 | found = true; 117 | } 118 | Assert.assertNotSame(message, isNewInstance, found); 119 | } 120 | 121 | /** 122 | * Asserts that the available memory is not considered low by the system. 123 | */ 124 | 125 | public void assertMemoryNotLow() { 126 | ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); 127 | ((ActivityManager)activityUtils.getCurrentActivity().getSystemService("activity")).getMemoryInfo(mi); 128 | Assert.assertFalse("Low memory available: " + mi.availMem + " bytes!", mi.lowMemory); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/By.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | /** 4 | * Used in conjunction with the web methods. Examples are By.id(String id) and By.cssSelector(String selector). 5 | * 6 | * @author Renas Reda, renas.reda@robotium.com 7 | * 8 | */ 9 | 10 | public abstract class By { 11 | 12 | /** 13 | * Select a WebElement by its id. 14 | * 15 | * @param id the id of the web element 16 | * @return the Id object 17 | */ 18 | 19 | public static By id(final String id) { 20 | return new Id(id); 21 | 22 | } 23 | 24 | /** 25 | * Select a WebElement by its xpath. 26 | * 27 | * @param xpath the xpath of the web element 28 | * @return the Xpath object 29 | */ 30 | 31 | public static By xpath(final String xpath) { 32 | return new Xpath(xpath); 33 | 34 | } 35 | 36 | /** 37 | * Select a WebElement by its css selector. 38 | * 39 | * @param selectors the css selector of the web element 40 | * @return the CssSelector object 41 | */ 42 | 43 | public static By cssSelector(final String selectors) { 44 | return new CssSelector(selectors); 45 | 46 | } 47 | 48 | /** 49 | * Select a WebElement by its name. 50 | * 51 | * @param name the name of the web element 52 | * @return the Name object 53 | */ 54 | 55 | public static By name(final String name) { 56 | return new Name(name); 57 | 58 | } 59 | 60 | /** 61 | * Select a WebElement by its class name. 62 | * 63 | * @param className the class name of the web element 64 | * @return the ClassName object 65 | */ 66 | 67 | public static By className(final String className) { 68 | return new ClassName(className); 69 | 70 | } 71 | 72 | /** 73 | * Select a WebElement by its text content. 74 | * 75 | * @param textContent the text content of the web element 76 | * @return the TextContent object 77 | */ 78 | 79 | public static By textContent(final String textContent) { 80 | return new Text(textContent); 81 | 82 | } 83 | 84 | /** 85 | * Select a WebElement by its tag name. 86 | * 87 | * @param tagName the tag name of the web element 88 | * @return the TagName object 89 | */ 90 | 91 | public static By tagName(final String tagName) { 92 | return new TagName(tagName); 93 | 94 | } 95 | 96 | /** 97 | * Returns the value. 98 | * 99 | * @return the value 100 | */ 101 | 102 | public String getValue(){ 103 | return ""; 104 | } 105 | 106 | 107 | static class Id extends By { 108 | private final String id; 109 | 110 | public Id(String id) { 111 | this.id = id; 112 | } 113 | 114 | @Override 115 | public String getValue(){ 116 | return id; 117 | } 118 | } 119 | 120 | static class Xpath extends By { 121 | private final String xpath; 122 | 123 | public Xpath(String xpath) { 124 | this.xpath = xpath; 125 | } 126 | 127 | @Override 128 | public String getValue(){ 129 | return xpath; 130 | } 131 | } 132 | 133 | static class CssSelector extends By { 134 | private final String selector; 135 | 136 | public CssSelector(String selector) { 137 | this.selector = selector; 138 | } 139 | 140 | @Override 141 | public String getValue(){ 142 | return selector; 143 | } 144 | } 145 | 146 | static class Name extends By { 147 | private final String name; 148 | 149 | public Name(String name) { 150 | this.name = name; 151 | } 152 | 153 | @Override 154 | public String getValue(){ 155 | return name; 156 | } 157 | } 158 | 159 | static class ClassName extends By { 160 | private final String className; 161 | 162 | public ClassName(String className) { 163 | this.className = className; 164 | } 165 | 166 | @Override 167 | public String getValue(){ 168 | return className; 169 | } 170 | } 171 | 172 | static class Text extends By { 173 | private final String textContent; 174 | 175 | public Text(String textContent) { 176 | this.textContent = textContent; 177 | } 178 | 179 | @Override 180 | public String getValue(){ 181 | return textContent; 182 | } 183 | } 184 | 185 | static class TagName extends By { 186 | private final String tagName; 187 | 188 | public TagName(String tagName){ 189 | this.tagName = tagName; 190 | } 191 | 192 | @Override 193 | public String getValue(){ 194 | return tagName; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Checker.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import android.widget.CheckedTextView; 5 | import android.widget.CompoundButton; 6 | import android.widget.Spinner; 7 | import android.widget.TextView; 8 | 9 | 10 | /** 11 | * Contains various check methods. Examples are: isButtonChecked(), 12 | * isSpinnerTextSelected. 13 | * 14 | * @author Renas Reda, renas.reda@robotium.com 15 | * 16 | */ 17 | 18 | class Checker { 19 | 20 | private final ViewFetcher viewFetcher; 21 | private final Waiter waiter; 22 | 23 | /** 24 | * Constructs this object. 25 | * 26 | * @param viewFetcher the {@code ViewFetcher} instance 27 | * @param waiter the {@code Waiter} instance 28 | */ 29 | 30 | public Checker(ViewFetcher viewFetcher, Waiter waiter){ 31 | this.viewFetcher = viewFetcher; 32 | this.waiter = waiter; 33 | } 34 | 35 | 36 | /** 37 | * Checks if a {@link CompoundButton} with a given index is checked. 38 | * 39 | * @param expectedClass the expected class, e.g. {@code CheckBox.class} or {@code RadioButton.class} 40 | * @param index of the {@code CompoundButton} to check. {@code 0} if only one is available 41 | * @return {@code true} if {@code CompoundButton} is checked and {@code false} if it is not checked 42 | */ 43 | 44 | public boolean isButtonChecked(Class expectedClass, int index) 45 | { 46 | return (waiter.waitForAndGetView(index, expectedClass).isChecked()); 47 | } 48 | 49 | /** 50 | * Checks if a {@link CompoundButton} with a given text is checked. 51 | * 52 | * @param expectedClass the expected class, e.g. {@code CheckBox.class} or {@code RadioButton.class} 53 | * @param text the text that is expected to be checked 54 | * @return {@code true} if {@code CompoundButton} is checked and {@code false} if it is not checked 55 | */ 56 | 57 | public boolean isButtonChecked(Class expectedClass, String text) 58 | { 59 | T button = waiter.waitForText(expectedClass, text, 0, Timeout.getSmallTimeout(), true); 60 | 61 | if(button != null && button.isChecked()){ 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | /** 68 | * Checks if a {@link CheckedTextView} with a given text is checked. 69 | * 70 | * @param checkedTextView the {@code CheckedTextView} object 71 | * @param text the text that is expected to be checked 72 | * @return {@code true} if {@code CheckedTextView} is checked and {@code false} if it is not checked 73 | */ 74 | 75 | public boolean isCheckedTextChecked(String text) 76 | { 77 | CheckedTextView checkedTextView = waiter.waitForText(CheckedTextView.class, text, 0, Timeout.getSmallTimeout(), true); 78 | 79 | if(checkedTextView != null && checkedTextView.isChecked()) { 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | 86 | /** 87 | * Checks if a given text is selected in any {@link Spinner} located on the current screen. 88 | * 89 | * @param text the text that is expected to be selected 90 | * @return {@code true} if the given text is selected in any {@code Spinner} and false if it is not 91 | */ 92 | 93 | public boolean isSpinnerTextSelected(String text) 94 | { 95 | waiter.waitForAndGetView(0, Spinner.class); 96 | 97 | ArrayList spinnerList = viewFetcher.getCurrentViews(Spinner.class, true); 98 | for(int i = 0; i < spinnerList.size(); i++){ 99 | if(isSpinnerTextSelected(i, text)) 100 | return true; 101 | } 102 | return false; 103 | } 104 | 105 | /** 106 | * Checks if a given text is selected in a given {@link Spinner} 107 | * @param spinnerIndex the index of the spinner to check. 0 if only one spinner is available 108 | * @param text the text that is expected to be selected 109 | * @return true if the given text is selected in the given {@code Spinner} and false if it is not 110 | */ 111 | 112 | public boolean isSpinnerTextSelected(int spinnerIndex, String text) 113 | { 114 | Spinner spinner = waiter.waitForAndGetView(spinnerIndex, Spinner.class); 115 | 116 | TextView textView = (TextView) spinner.getChildAt(0); 117 | if(textView.getText().equals(text)) 118 | return true; 119 | else 120 | return false; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Condition.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | /** 4 | * Represents a conditional statement.
5 | * Implementations may be used with {@link Solo#waitForCondition(Condition, int)}. 6 | */ 7 | public interface Condition { 8 | 9 | /** 10 | * Should do the necessary work needed to check a condition and then return whether this condition is satisfied or not. 11 | * @return {@code true} if condition is satisfied and {@code false} if it is not satisfied 12 | */ 13 | public boolean isSatisfied(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/DialogUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | 4 | import android.app.Activity; 5 | import android.app.Instrumentation; 6 | import android.content.Context; 7 | import android.os.SystemClock; 8 | import android.view.ContextThemeWrapper; 9 | import android.view.View; 10 | import android.view.inputmethod.InputMethodManager; 11 | import android.widget.EditText; 12 | 13 | 14 | /** 15 | * Contains the waitForDialogToClose() method. 16 | * 17 | * @author Renas Reda, renas.reda@robotium.com 18 | * 19 | */ 20 | 21 | class DialogUtils { 22 | 23 | private final Instrumentation instrumentation; 24 | private final ActivityUtils activityUtils; 25 | private final ViewFetcher viewFetcher; 26 | private final Sleeper sleeper; 27 | private final static int TIMEOUT_DIALOG_TO_CLOSE = 1000; 28 | private final int MINISLEEP = 200; 29 | 30 | /** 31 | * Constructs this object. 32 | * 33 | * @param activityUtils the {@code ActivityUtils} instance 34 | * @param viewFetcher the {@code ViewFetcher} instance 35 | * @param sleeper the {@code Sleeper} instance 36 | */ 37 | 38 | public DialogUtils(Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) { 39 | this.instrumentation = instrumentation; 40 | this.activityUtils = activityUtils; 41 | this.viewFetcher = viewFetcher; 42 | this.sleeper = sleeper; 43 | } 44 | 45 | 46 | /** 47 | * Waits for a {@link android.app.Dialog} to close. 48 | * 49 | * @param timeout the amount of time in milliseconds to wait 50 | * @return {@code true} if the {@code Dialog} is closed before the timeout and {@code false} if it is not closed 51 | */ 52 | 53 | public boolean waitForDialogToClose(long timeout) { 54 | waitForDialogToOpen(TIMEOUT_DIALOG_TO_CLOSE, false); 55 | final long endTime = SystemClock.uptimeMillis() + timeout; 56 | 57 | while (SystemClock.uptimeMillis() < endTime) { 58 | 59 | if(!isDialogOpen()){ 60 | return true; 61 | } 62 | sleeper.sleep(MINISLEEP); 63 | } 64 | return false; 65 | } 66 | 67 | 68 | 69 | /** 70 | * Waits for a {@link android.app.Dialog} to open. 71 | * 72 | * @param timeout the amount of time in milliseconds to wait 73 | * @return {@code true} if the {@code Dialog} is opened before the timeout and {@code false} if it is not opened 74 | */ 75 | 76 | public boolean waitForDialogToOpen(long timeout, boolean sleepFirst) { 77 | final long endTime = SystemClock.uptimeMillis() + timeout; 78 | boolean dialogIsOpen = isDialogOpen(); 79 | 80 | if(sleepFirst) 81 | sleeper.sleep(); 82 | 83 | if(dialogIsOpen){ 84 | return true; 85 | } 86 | 87 | while (SystemClock.uptimeMillis() < endTime) { 88 | 89 | if(isDialogOpen()){ 90 | return true; 91 | } 92 | sleeper.sleepMini(); 93 | } 94 | return false; 95 | } 96 | 97 | /** 98 | * Checks if a dialog is open. 99 | * 100 | * @return true if dialog is open 101 | */ 102 | 103 | private boolean isDialogOpen(){ 104 | final Activity activity = activityUtils.getCurrentActivity(false); 105 | final View[] views = viewFetcher.getWindowDecorViews(); 106 | View view = viewFetcher.getRecentDecorView(views); 107 | 108 | if(!isDialog(activity, view)){ 109 | for(View v : views){ 110 | if(isDialog(activity, v)){ 111 | return true; 112 | } 113 | } 114 | } 115 | else { 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | /** 122 | * Checks that the specified DecorView and the Activity DecorView are not equal. 123 | * 124 | * @param activity the activity which DecorView is to be compared 125 | * @param decorView the DecorView to compare 126 | * @return true if not equal 127 | */ 128 | 129 | private boolean isDialog(Activity activity, View decorView){ 130 | if(decorView == null || !decorView.isShown() || activity == null){ 131 | return false; 132 | } 133 | Context viewContext = null; 134 | if(decorView != null){ 135 | viewContext = decorView.getContext(); 136 | } 137 | 138 | if (viewContext instanceof ContextThemeWrapper) { 139 | ContextThemeWrapper ctw = (ContextThemeWrapper) viewContext; 140 | viewContext = ctw.getBaseContext(); 141 | } 142 | Context activityContext = activity; 143 | Context activityBaseContext = activity.getBaseContext(); 144 | return (activityContext.equals(viewContext) || activityBaseContext.equals(viewContext)) && (decorView != activity.getWindow().getDecorView()); 145 | } 146 | 147 | /** 148 | * Hides the soft keyboard 149 | * 150 | * @param shouldSleepFirst whether to sleep a default pause first 151 | * @param shouldSleepAfter whether to sleep a default pause after 152 | */ 153 | 154 | public void hideSoftKeyboard(EditText editText, boolean shouldSleepFirst, boolean shouldSleepAfter) { 155 | InputMethodManager inputMethodManager; 156 | 157 | Activity activity = activityUtils.getCurrentActivity(shouldSleepFirst); 158 | if(activity == null){ 159 | inputMethodManager = (InputMethodManager) instrumentation.getTargetContext().getSystemService(Context.INPUT_METHOD_SERVICE); 160 | } 161 | else { 162 | inputMethodManager = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); 163 | } 164 | 165 | if(editText != null) { 166 | inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0); 167 | return; 168 | } 169 | View focusedView = activity.getCurrentFocus(); 170 | 171 | if(!(focusedView instanceof EditText)) { 172 | EditText freshestEditText = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(EditText.class, true)); 173 | if(freshestEditText != null){ 174 | focusedView = freshestEditText; 175 | } 176 | } 177 | if(focusedView != null) { 178 | inputMethodManager.hideSoftInputFromWindow(focusedView.getWindowToken(), 0); 179 | } 180 | if(shouldSleepAfter){ 181 | sleeper.sleep(); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/GLRenderWrapper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.nio.IntBuffer; 4 | import java.util.concurrent.CountDownLatch; 5 | import javax.microedition.khronos.egl.EGLConfig; 6 | import javax.microedition.khronos.opengles.GL10; 7 | import android.graphics.Bitmap; 8 | import android.opengl.GLES20; 9 | import android.opengl.GLSurfaceView; 10 | import android.opengl.GLSurfaceView.Renderer; 11 | import android.view.View; 12 | 13 | /** 14 | * Used to wrap and replace the renderer to gain access to the gl context. 15 | * 16 | * @author Per-Erik Bergman, bergman@uncle.se 17 | * 18 | */ 19 | 20 | class GLRenderWrapper implements Renderer { 21 | 22 | private Renderer renderer; 23 | private int width; 24 | private int height; 25 | private final GLSurfaceView view; 26 | private CountDownLatch latch; 27 | private boolean takeScreenshot = true; 28 | private int glVersion; 29 | 30 | /** 31 | * Constructs this object. 32 | * 33 | * @param view the current glSurfaceView 34 | * @param renderer the renderer to wrap 35 | * @param latch the count down latch 36 | */ 37 | 38 | public GLRenderWrapper(GLSurfaceView view, 39 | Renderer renderer, CountDownLatch latch) { 40 | this.view = view; 41 | this.renderer = renderer; 42 | this.latch = latch; 43 | 44 | this.width = view.getWidth(); 45 | this.height = view.getHeight(); 46 | 47 | Integer out = new Reflect(view).field("mEGLContextClientVersion") 48 | .out(Integer.class); 49 | if ( out != null ) { 50 | this.glVersion = out.intValue(); 51 | } else { 52 | this.glVersion = -1; 53 | this.takeScreenshot = false; 54 | } 55 | } 56 | 57 | @Override 58 | /* 59 | * (non-Javadoc) 60 | * @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(javax.microedition.khronos.opengles.GL10, javax.microedition.khronos.egl.EGLConfig) 61 | */ 62 | 63 | public void onSurfaceCreated(GL10 gl, EGLConfig config) { 64 | renderer.onSurfaceCreated(gl, config); 65 | } 66 | 67 | @Override 68 | /* 69 | * (non-Javadoc) 70 | * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int) 71 | */ 72 | 73 | public void onSurfaceChanged(GL10 gl, int width, int height) { 74 | this.width = width; 75 | this.height = height; 76 | renderer.onSurfaceChanged(gl, width, height); 77 | } 78 | 79 | @Override 80 | /* 81 | * (non-Javadoc) 82 | * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(javax.microedition.khronos.opengles.GL10) 83 | */ 84 | 85 | public void onDrawFrame(GL10 gl) { 86 | renderer.onDrawFrame(gl); 87 | if (takeScreenshot) { 88 | Bitmap screenshot = null; 89 | 90 | if (glVersion >= 2) { 91 | screenshot = savePixels(0, 0, width, height); 92 | } else { 93 | screenshot = savePixels(0, 0, width, height, gl); 94 | } 95 | 96 | new Reflect(view).field("mDrawingCache").type(View.class) 97 | .in(screenshot); 98 | latch.countDown(); 99 | takeScreenshot = false; 100 | } 101 | } 102 | 103 | /** 104 | * Tell the wrapper to take a screen shot 105 | */ 106 | 107 | public void setTakeScreenshot() { 108 | takeScreenshot = true; 109 | } 110 | 111 | /** 112 | * Set the count down latch 113 | */ 114 | 115 | public void setLatch(CountDownLatch latch) { 116 | this.latch = latch; 117 | } 118 | 119 | /** 120 | * Extract the bitmap from OpenGL 121 | * 122 | * @param x the start column 123 | * @param y the start line 124 | * @param w the width of the bitmap 125 | * @param h the height of the bitmap 126 | */ 127 | 128 | private Bitmap savePixels(int x, int y, int w, int h) { 129 | int b[] = new int[w * (y + h)]; 130 | int bt[] = new int[w * h]; 131 | IntBuffer ib = IntBuffer.wrap(b); 132 | ib.position(0); 133 | GLES20.glReadPixels(x, 0, w, y + h, GLES20.GL_RGBA, 134 | GLES20.GL_UNSIGNED_BYTE, ib); 135 | 136 | for (int i = 0, k = 0; i < h; i++, k++) { 137 | // remember, that OpenGL bitmap is incompatible with Android bitmap 138 | // and so, some correction need. 139 | for (int j = 0; j < w; j++) { 140 | int pix = b[i * w + j]; 141 | int pb = (pix >> 16) & 0xff; 142 | int pr = (pix << 16) & 0x00ff0000; 143 | int pix1 = (pix & 0xff00ff00) | pr | pb; 144 | bt[(h - k - 1) * w + j] = pix1; 145 | } 146 | } 147 | 148 | Bitmap sb = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888); 149 | return sb; 150 | } 151 | 152 | /** 153 | * Extract the bitmap from OpenGL 154 | * 155 | * @param x the start column 156 | * @param y the start line 157 | * @param w the width of the bitmap 158 | * @param h the height of the bitmap 159 | * @param gl the current GL reference 160 | */ 161 | 162 | private static Bitmap savePixels(int x, int y, int w, int h, GL10 gl) { 163 | int b[] = new int[w * (y + h)]; 164 | int bt[] = new int[w * h]; 165 | IntBuffer ib = IntBuffer.wrap(b); 166 | ib.position(0); 167 | gl.glReadPixels(x, 0, w, y + h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, ib); 168 | 169 | for (int i = 0, k = 0; i < h; i++, k++) { 170 | // remember, that OpenGL bitmap is incompatible with Android bitmap 171 | // and so, some correction need. 172 | for (int j = 0; j < w; j++) { 173 | int pix = b[i * w + j]; 174 | int pb = (pix >> 16) & 0xff; 175 | int pr = (pix << 16) & 0x00ff0000; 176 | int pix1 = (pix & 0xff00ff00) | pr | pb; 177 | bt[(h - k - 1) * w + j] = pix1; 178 | } 179 | } 180 | 181 | Bitmap sb = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888); 182 | return sb; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Getter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Activity; 5 | import android.app.Instrumentation; 6 | import android.content.Context; 7 | import android.view.View; 8 | import android.view.Window; 9 | import android.widget.TextView; 10 | 11 | 12 | /** 13 | * Contains various get methods. Examples are: getView(int id), 14 | * getView(Class classToFilterBy, int index). 15 | * 16 | * @author Renas Reda, renas.reda@robotium.com 17 | * 18 | */ 19 | 20 | class Getter { 21 | 22 | private final Instrumentation instrumentation; 23 | private final ActivityUtils activityUtils; 24 | private final Waiter waiter; 25 | private final int TIMEOUT = 1000; 26 | 27 | /** 28 | * Constructs this object. 29 | * 30 | * @param inst the {@code Instrumentation} instance 31 | * @param viewFetcher the {@code ViewFetcher} instance 32 | * @param waiter the {@code Waiter} instance 33 | */ 34 | 35 | public Getter(Instrumentation instrumentation, ActivityUtils activityUtils, Waiter waiter){ 36 | this.instrumentation = instrumentation; 37 | this.activityUtils = activityUtils; 38 | this.waiter = waiter; 39 | } 40 | 41 | 42 | /** 43 | * Returns a {@code View} with a certain index, from the list of current {@code View}s of the specified type. 44 | * 45 | * @param classToFilterBy which {@code View}s to choose from 46 | * @param index choose among all instances of this type, e.g. {@code Button.class} or {@code EditText.class} 47 | * @return a {@code View} with a certain index, from the list of current {@code View}s of the specified type 48 | */ 49 | 50 | public T getView(Class classToFilterBy, int index) { 51 | return waiter.waitForAndGetView(index, classToFilterBy); 52 | } 53 | 54 | /** 55 | * Returns a {@code View} that shows a given text, from the list of current {@code View}s of the specified type. 56 | * 57 | * @param classToFilterBy which {@code View}s to choose from 58 | * @param text the text that the view shows 59 | * @param onlyVisible {@code true} if only visible texts on the screen should be returned 60 | * @return a {@code View} showing a given text, from the list of current {@code View}s of the specified type 61 | */ 62 | 63 | public T getView(Class classToFilterBy, String text, boolean onlyVisible) { 64 | 65 | T viewToReturn = (T) waiter.waitForText(classToFilterBy, text, 0, Timeout.getSmallTimeout(), false, onlyVisible, false); 66 | 67 | if(viewToReturn == null) 68 | Assert.fail(classToFilterBy.getSimpleName() + " with text: '" + text + "' is not found!"); 69 | 70 | return viewToReturn; 71 | } 72 | 73 | /** 74 | * Returns a localized string 75 | * 76 | * @param id the resource ID for the string 77 | * @return the localized string 78 | */ 79 | 80 | public String getString(int id) 81 | { 82 | Activity activity = activityUtils.getCurrentActivity(false); 83 | if(activity == null){ 84 | return ""; 85 | } 86 | return activity.getString(id); 87 | } 88 | 89 | /** 90 | * Returns a localized string 91 | * 92 | * @param id the resource ID for the string 93 | * @return the localized string 94 | */ 95 | 96 | public String getString(String id) 97 | { 98 | Context targetContext = instrumentation.getTargetContext(); 99 | String packageName = targetContext.getPackageName(); 100 | int viewId = targetContext.getResources().getIdentifier(id, "string", packageName); 101 | if(viewId == 0){ 102 | viewId = targetContext.getResources().getIdentifier(id, "string", "android"); 103 | } 104 | return getString(viewId); 105 | } 106 | 107 | /** 108 | * Returns a {@code View} with a given id. 109 | * 110 | * @param id the R.id of the {@code View} to be returned 111 | * @param index the index of the {@link View}. {@code 0} if only one is available 112 | * @param timeout the timeout in milliseconds 113 | * @return a {@code View} with a given id 114 | */ 115 | 116 | public View getView(int id, int index, int timeout){ 117 | return waiter.waitForView(id, index, timeout); 118 | } 119 | 120 | /** 121 | * Returns a {@code View} with a given id. 122 | * 123 | * @param id the R.id of the {@code View} to be returned 124 | * @param index the index of the {@link View}. {@code 0} if only one is available 125 | * @return a {@code View} with a given id 126 | */ 127 | 128 | public View getView(int id, int index){ 129 | return getView(id, index, 0); 130 | } 131 | 132 | /** 133 | * Returns a {@code View} with a given id. 134 | * 135 | * @param id the id of the {@link View} to return 136 | * @param index the index of the {@link View}. {@code 0} if only one is available 137 | * @return a {@code View} with a given id 138 | */ 139 | 140 | public View getView(String id, int index){ 141 | View viewToReturn = null; 142 | Context targetContext = instrumentation.getTargetContext(); 143 | String packageName = targetContext.getPackageName(); 144 | int viewId = targetContext.getResources().getIdentifier(id, "id", packageName); 145 | 146 | if(viewId != 0){ 147 | viewToReturn = getView(viewId, index, TIMEOUT); 148 | } 149 | 150 | if(viewToReturn == null){ 151 | int androidViewId = targetContext.getResources().getIdentifier(id, "id", "android"); 152 | if(androidViewId != 0){ 153 | viewToReturn = getView(androidViewId, index, TIMEOUT); 154 | } 155 | } 156 | 157 | if(viewToReturn != null){ 158 | return viewToReturn; 159 | } 160 | return getView(viewId, index); 161 | } 162 | 163 | /** 164 | * Returns a {@code View} with a given tag. 165 | * 166 | * @param tag the tag of the {@code View} to be returned 167 | * @param index the index of the {@link View}. {@code 0} if only one is available 168 | * @param timeout the timeout in milliseconds 169 | * @return a {@code View} with a given tag if available, null otherwise 170 | */ 171 | 172 | public View getView(Object tag, int index, int timeout){ 173 | //Because https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/View.java#L17005-L17007 174 | if(tag == null) { 175 | return null; 176 | } 177 | 178 | final Activity activity = activityUtils.getCurrentActivity(false); 179 | View viewToReturn = null; 180 | 181 | if(index < 1){ 182 | index = 0; 183 | if(activity != null){ 184 | //Using https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/Activity.java#L2070-L2072 185 | Window window = activity.getWindow(); 186 | if(window != null) { 187 | View decorView = window.getDecorView(); 188 | if(decorView != null) { 189 | viewToReturn = decorView.findViewWithTag(tag); 190 | } 191 | } 192 | } 193 | } 194 | 195 | if (viewToReturn != null) { 196 | return viewToReturn; 197 | } 198 | 199 | return waiter.waitForView(tag, index, timeout); 200 | } 201 | 202 | /** 203 | * Returns a {@code View} with a given tag. 204 | * 205 | * @param tag the tag of the {@code View} to be returned 206 | * @param index the index of the {@link View}. {@code 0} if only one is available 207 | * @return a {@code View} with a given tag if available, null otherwise 208 | */ 209 | 210 | public View getView(Object tag, int index){ 211 | return getView(tag, index, 0); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Illustration.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * A class used to pass Illustrations to an Illustrator. 8 | * Compatible with specific MotionEvent.TOOL_TYPEs 9 | * 10 | * @author Jake Kuli, 3kajjak3@gmail.com 11 | */ 12 | public class Illustration { 13 | 14 | private final int toolType; 15 | private final ArrayList points; 16 | 17 | private Illustration(Builder builder) { 18 | this.toolType = builder.builderToolType; 19 | this.points = builder.builderPoints; 20 | } 21 | 22 | /** 23 | * Builder class to build illustrations 24 | */ 25 | public static class Builder { 26 | 27 | private int builderToolType = MotionEvent.TOOL_TYPE_FINGER; 28 | private ArrayList builderPoints = new ArrayList(); 29 | 30 | /** 31 | * Sets the tool type to use when illustrating. 32 | * By default this is set to MotionEvent.TOOL_TYPE_FINGER 33 | * @param toolType an int from MotionEvent's static int TOOL_TYPEs 34 | */ 35 | public Builder setToolType(int toolType) { 36 | builderToolType = toolType; 37 | return this; 38 | } 39 | 40 | public Builder addPoint(float x, float y, float pressure) { 41 | builderPoints.add(new PressurePoint(x, y, pressure)); 42 | return this; 43 | } 44 | 45 | public Illustration build() { 46 | return new Illustration(this); 47 | } 48 | } 49 | 50 | public ArrayList getPoints() { 51 | return points; 52 | } 53 | 54 | public int getToolType() { 55 | return toolType; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Illustrator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import android.view.MotionEvent; 5 | import android.view.MotionEvent.PointerProperties; 6 | import android.view.MotionEvent.PointerCoords; 7 | import android.view.InputDevice; 8 | import android.app.Instrumentation; 9 | import android.os.SystemClock; 10 | 11 | /** 12 | * A class that draws Illustrations to the screen 13 | * 14 | * @author Jake Kuli, 3kajjak3@gmail.com 15 | */ 16 | class Illustrator { 17 | 18 | private Instrumentation inst; 19 | 20 | public Illustrator(Instrumentation inst) { 21 | this.inst = inst; 22 | } 23 | 24 | public void illustrate(Illustration illustration) { 25 | if (illustration == null || illustration.getPoints().isEmpty()) { 26 | throw new IllegalArgumentException("Illustration must not be null and requires at least one point."); 27 | } 28 | MotionEvent event; 29 | int currentAction; 30 | long downTime = SystemClock.uptimeMillis(); 31 | long eventTime = SystemClock.uptimeMillis(); 32 | PointerCoords[] coords = new PointerCoords[1]; 33 | PointerCoords coord = new PointerCoords(); 34 | PointerProperties[] properties = new PointerProperties[1]; 35 | PointerProperties prop = new PointerProperties(); 36 | prop.id = 0; 37 | prop.toolType = illustration.getToolType(); 38 | properties[0] = prop; 39 | coords[0] = coord; 40 | ArrayList points = illustration.getPoints(); 41 | for (int i = 0; i < points.size(); i++) { 42 | PressurePoint currentPoint = points.get(i); 43 | coord.x = currentPoint.x; 44 | coord.y = currentPoint.y; 45 | coord.pressure = currentPoint.pressure; 46 | coord.size = 1; 47 | if (i == 0) { 48 | currentAction = MotionEvent.ACTION_DOWN; 49 | } 50 | else { 51 | currentAction = MotionEvent.ACTION_MOVE; 52 | } 53 | eventTime = SystemClock.uptimeMillis(); 54 | event = MotionEvent.obtain(downTime, 55 | eventTime, 56 | currentAction, 57 | 1, 58 | properties, 59 | coords, 60 | 0, 0, 1, 1, 0, 0, 61 | InputDevice.SOURCE_TOUCHSCREEN, 62 | 0); 63 | try { 64 | inst.sendPointerSync(event); 65 | } 66 | catch (SecurityException ignored) {} 67 | } 68 | currentAction = MotionEvent.ACTION_UP; 69 | coords[0] = coord; 70 | PressurePoint currentPoint = points.get(points.size() - 1); 71 | coord.x = currentPoint.x; 72 | coord.y = currentPoint.y; 73 | coord.pressure = currentPoint.pressure; 74 | coord.size = 1; 75 | eventTime = SystemClock.uptimeMillis(); 76 | event = MotionEvent.obtain(downTime, 77 | eventTime, 78 | currentAction, 79 | 1, 80 | properties, 81 | coords, 82 | 0, 0, 1, 1, 0, 0, 83 | InputDevice.SOURCE_TOUCHSCREEN, 84 | 0); 85 | try { 86 | inst.sendPointerSync(event); 87 | } 88 | catch (SecurityException ignored) {} 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Presser.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.widget.EditText; 4 | import android.widget.Spinner; 5 | import junit.framework.Assert; 6 | import android.app.Instrumentation; 7 | import android.view.KeyEvent; 8 | 9 | /** 10 | * Contains press methods. Examples are pressMenuItem(), 11 | * pressSpinnerItem(). 12 | * 13 | * @author Renas Reda, renas.reda@robotium.com 14 | * 15 | */ 16 | 17 | class Presser{ 18 | 19 | private final Clicker clicker; 20 | private final Instrumentation inst; 21 | private final Sleeper sleeper; 22 | private final Waiter waiter; 23 | private final DialogUtils dialogUtils; 24 | private final ViewFetcher viewFetcher; 25 | 26 | 27 | /** 28 | * Constructs this object. 29 | * 30 | * @param viewFetcher the {@code ViewFetcher} instance 31 | * @param clicker the {@code Clicker} instance 32 | * @param inst the {@code Instrumentation} instance 33 | * @param sleeper the {@code Sleeper} instance 34 | * @param waiter the {@code Waiter} instance 35 | * @param dialogUtils the {@code DialogUtils} instance 36 | */ 37 | 38 | public Presser(ViewFetcher viewFetcher, Clicker clicker, Instrumentation inst, Sleeper sleeper, Waiter waiter, DialogUtils dialogUtils) { 39 | this.viewFetcher = viewFetcher; 40 | this.clicker = clicker; 41 | this.inst = inst; 42 | this.sleeper = sleeper; 43 | this.waiter = waiter; 44 | this.dialogUtils = dialogUtils; 45 | } 46 | 47 | 48 | /** 49 | * Presses a {@link android.view.MenuItem} with a given index. Index {@code 0} is the first item in the 50 | * first row, Index {@code 3} is the first item in the second row and 51 | * index {@code 5} is the first item in the third row. 52 | * 53 | * @param index the index of the {@code MenuItem} to be pressed 54 | */ 55 | 56 | public void pressMenuItem(int index){ 57 | pressMenuItem(index, 3); 58 | } 59 | 60 | /** 61 | * Presses a {@link android.view.MenuItem} with a given index. Supports three rows with a given amount 62 | * of items. If itemsPerRow equals 5 then index 0 is the first item in the first row, 63 | * index 5 is the first item in the second row and index 10 is the first item in the third row. 64 | * 65 | * @param index the index of the {@code MenuItem} to be pressed 66 | * @param itemsPerRow the amount of menu items there are per row. 67 | */ 68 | 69 | public void pressMenuItem(int index, int itemsPerRow) { 70 | int[] row = new int[4]; 71 | for(int i = 1; i <=3; i++) 72 | row[i] = itemsPerRow*i; 73 | 74 | sleeper.sleep(); 75 | try{ 76 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); 77 | dialogUtils.waitForDialogToOpen(Timeout.getSmallTimeout(), true); 78 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 79 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 80 | }catch(SecurityException e){ 81 | Assert.fail("Can not press the menu!"); 82 | } 83 | if (index < row[1]) { 84 | for (int i = 0; i < index; i++) { 85 | sleeper.sleepMini(); 86 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 87 | } 88 | } else if (index >= row[1] && index < row[2]) { 89 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 90 | 91 | for (int i = row[1]; i < index; i++) { 92 | sleeper.sleepMini(); 93 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 94 | } 95 | } else if (index >= row[2]) { 96 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 97 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 98 | 99 | for (int i = row[2]; i < index; i++) { 100 | sleeper.sleepMini(); 101 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT); 102 | } 103 | } 104 | 105 | try{ 106 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER); 107 | }catch (SecurityException ignored) {} 108 | } 109 | 110 | /** 111 | * Presses the soft keyboard search/next button. 112 | * 113 | * @param imeAction the action to be performed 114 | * 115 | */ 116 | 117 | public void pressSoftKeyboard(final int imeAction){ 118 | final EditText freshestEditText = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(EditText.class, true)); 119 | if(freshestEditText != null){ 120 | inst.runOnMainSync(new Runnable() 121 | { 122 | public void run() 123 | { 124 | freshestEditText.onEditorAction(imeAction); 125 | 126 | } 127 | }); 128 | } 129 | } 130 | 131 | /** 132 | * Presses on a {@link android.widget.Spinner} (drop-down menu) item. 133 | * 134 | * @param spinnerIndex the index of the {@code Spinner} menu to be used 135 | * @param itemIndex the index of the {@code Spinner} item to be pressed relative to the currently selected item. 136 | * A Negative number moves up on the {@code Spinner}, positive moves down 137 | */ 138 | 139 | public void pressSpinnerItem(int spinnerIndex, int itemIndex) 140 | { 141 | clicker.clickOnScreen(waiter.waitForAndGetView(spinnerIndex, Spinner.class)); 142 | dialogUtils.waitForDialogToOpen(Timeout.getSmallTimeout(), true); 143 | 144 | try{ 145 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 146 | }catch(SecurityException ignored){} 147 | 148 | boolean countingUp = true; 149 | if(itemIndex < 0){ 150 | countingUp = false; 151 | itemIndex *= -1; 152 | } 153 | for(int i = 0; i < itemIndex; i++) 154 | { 155 | sleeper.sleepMini(); 156 | if(countingUp){ 157 | try{ 158 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN); 159 | }catch(SecurityException ignored){} 160 | }else{ 161 | try{ 162 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP); 163 | }catch(SecurityException ignored){} 164 | } 165 | } 166 | try{ 167 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER); 168 | }catch(SecurityException ignored){} 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/PressurePoint.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | /** 4 | * A class to describe points and respective pressures in an Illustration 5 | * 6 | * @author Jake Kuli, 3kajjak3@gmail.com 7 | */ 8 | class PressurePoint { 9 | public final float x; 10 | public final float y; 11 | public final float pressure; 12 | 13 | public PressurePoint(float x, float y, float pressure) { 14 | this.x = x; 15 | this.y = y; 16 | this.pressure = pressure; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Reflect.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | /** 6 | * A reflection utility class. 7 | * 8 | * @author Per-Erik Bergman, bergman@uncle.se 9 | * 10 | */ 11 | 12 | class Reflect { 13 | private Object object; 14 | 15 | /** 16 | * Constructs this object 17 | * 18 | * @param object the object to reflect on 19 | */ 20 | 21 | public Reflect(Object object) { 22 | if (object == null) 23 | throw new IllegalArgumentException("Object can not be null."); 24 | this.object = object; 25 | } 26 | 27 | /** 28 | * Get a field from the object 29 | * 30 | * @param name the name of the field 31 | * 32 | * @return a field reference 33 | */ 34 | 35 | public FieldRf field(String name) { 36 | return new FieldRf(object, name); 37 | } 38 | 39 | /** 40 | * A field reference. 41 | */ 42 | public class FieldRf { 43 | private Class clazz; 44 | private Object object; 45 | private String name; 46 | 47 | /** 48 | * Constructs this object 49 | * 50 | * @param object the object to reflect on 51 | * @param name the name of the field 52 | */ 53 | 54 | public FieldRf(Object object, String name) { 55 | this.object = object; 56 | this.name = name; 57 | } 58 | 59 | /** 60 | * Constructs this object 61 | * 62 | * @param outclazz the output type 63 | * 64 | * @return T 65 | */ 66 | 67 | public T out(Class outclazz) { 68 | Field field = getField(); 69 | Object obj = getValue(field); 70 | return outclazz.cast(obj); 71 | } 72 | 73 | /** 74 | * Set a value to a field 75 | * 76 | * @param value the value to set 77 | */ 78 | 79 | public void in(Object value) { 80 | Field field = getField(); 81 | try { 82 | field.set(object, value); 83 | } catch (IllegalArgumentException e) { 84 | e.printStackTrace(); 85 | } catch (IllegalAccessException e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | 90 | /** 91 | * Set the class type 92 | * 93 | * @param clazz the type 94 | * 95 | * @return a field reference 96 | */ 97 | 98 | public FieldRf type(Class clazz) { 99 | this.clazz = clazz; 100 | return this; 101 | } 102 | 103 | private Field getField() { 104 | if (clazz == null) { 105 | clazz = object.getClass(); 106 | } 107 | 108 | Field field = null; 109 | try { 110 | field = clazz.getDeclaredField(name); 111 | field.setAccessible(true); 112 | } catch (NoSuchFieldException ignored) {} 113 | return field; 114 | } 115 | 116 | private Object getValue(Field field) { 117 | if (field == null) { 118 | return null; 119 | } 120 | Object obj = null; 121 | try { 122 | obj = field.get(object); 123 | } catch (IllegalArgumentException e) { 124 | e.printStackTrace(); 125 | } catch (IllegalAccessException e) { 126 | e.printStackTrace(); 127 | } 128 | return obj; 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/RobotiumTextView.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.content.Context; 4 | import android.widget.TextView; 5 | 6 | /** 7 | * Used to create a TextView object that is based on a web element. Contains the web element text and location. 8 | * 9 | * @author Renas Reda, renas.reda@robotium.com 10 | * 11 | */ 12 | 13 | class RobotiumTextView extends TextView { 14 | private int locationX = 0; 15 | private int locationY = 0; 16 | 17 | /** 18 | * Constructs this object 19 | * 20 | * @param context the given context 21 | */ 22 | 23 | public RobotiumTextView(Context context){ 24 | super(context); 25 | } 26 | 27 | /** 28 | * Constructs this object 29 | * 30 | * @param context the given context 31 | * @param text the given text to be set 32 | */ 33 | 34 | public RobotiumTextView(Context context, String text, int locationX, int locationY) { 35 | super(context); 36 | this.setText(text); 37 | setLocationX(locationX); 38 | setLocationY(locationY); 39 | } 40 | 41 | /** 42 | * Returns the location on screen of the {@code TextView} that is based on a web element 43 | */ 44 | 45 | @Override 46 | public void getLocationOnScreen(int[] location) { 47 | 48 | location[0] = locationX; 49 | location[1] = locationY; 50 | } 51 | 52 | /** 53 | * Sets the X location of the TextView 54 | * 55 | * @param locationX the X location of the {@code TextView} 56 | */ 57 | 58 | public void setLocationX(int locationX){ 59 | this.locationX = locationX; 60 | } 61 | 62 | 63 | /** 64 | * Sets the Y location 65 | * 66 | * @param locationY the Y location of the {@code TextView} 67 | */ 68 | 69 | public void setLocationY(int locationY){ 70 | this.locationY = locationY; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/RobotiumUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | import java.util.regex.PatternSyntaxException; 10 | 11 | import android.view.View; 12 | import android.widget.TextView; 13 | 14 | /** 15 | * Contains utility methods. Examples are: removeInvisibleViews(Iterable viewList), 16 | * filterViews(Class classToFilterBy, Iterable viewList), sortViewsByLocationOnScreen(List views). 17 | * 18 | * @author Renas Reda, renas.reda@robotium.com 19 | * 20 | */ 21 | 22 | public class RobotiumUtils { 23 | 24 | 25 | /** 26 | * Removes invisible Views. 27 | * 28 | * @param viewList an Iterable with Views that is being checked for invisible Views 29 | * @return a filtered Iterable with no invisible Views 30 | */ 31 | 32 | public static ArrayList removeInvisibleViews(Iterable viewList) { 33 | ArrayList tmpViewList = new ArrayList(); 34 | for (T view : viewList) { 35 | if (view != null && view.isShown()) { 36 | tmpViewList.add(view); 37 | } 38 | } 39 | return tmpViewList; 40 | } 41 | 42 | /** 43 | * Filters Views based on the given class type. 44 | * 45 | * @param classToFilterBy the class to filter 46 | * @param viewList the Iterable to filter from 47 | * @return an ArrayList with filtered views 48 | */ 49 | 50 | public static ArrayList filterViews(Class classToFilterBy, Iterable viewList) { 51 | ArrayList filteredViews = new ArrayList(); 52 | for (Object view : viewList) { 53 | if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { 54 | filteredViews.add(classToFilterBy.cast(view)); 55 | } 56 | } 57 | viewList = null; 58 | return filteredViews; 59 | } 60 | 61 | /** 62 | * Filters all Views not within the given set. 63 | * 64 | * @param classSet contains all classes that are ok to pass the filter 65 | * @param viewList the Iterable to filter form 66 | * @return an ArrayList with filtered views 67 | */ 68 | 69 | public static ArrayList filterViewsToSet(Class classSet[], Iterable viewList) { 70 | ArrayList filteredViews = new ArrayList(); 71 | for (View view : viewList) { 72 | if (view == null) 73 | continue; 74 | for (Class filter : classSet) { 75 | if (filter.isAssignableFrom(view.getClass())) { 76 | filteredViews.add(view); 77 | break; 78 | } 79 | } 80 | } 81 | return filteredViews; 82 | } 83 | 84 | /** 85 | * Orders Views by their location on-screen. 86 | * 87 | * @param views The views to sort 88 | * @see ViewLocationComparator 89 | */ 90 | 91 | public static void sortViewsByLocationOnScreen(List views) { 92 | Collections.sort(views, new ViewLocationComparator()); 93 | } 94 | 95 | /** 96 | * Orders Views by their location on-screen. 97 | * 98 | * @param views The views to sort 99 | * @param yAxisFirst Whether the y-axis should be compared before the x-axis 100 | * @see ViewLocationComparator 101 | */ 102 | 103 | public static void sortViewsByLocationOnScreen(List views, boolean yAxisFirst) { 104 | Collections.sort(views, new ViewLocationComparator(yAxisFirst)); 105 | } 106 | 107 | /** 108 | * Checks if a View matches a certain string and returns the amount of total matches. 109 | * 110 | * @param regex the regex to match 111 | * @param view the view to check 112 | * @param uniqueTextViews set of views that have matched 113 | * @return number of total matches 114 | */ 115 | 116 | public static int getNumberOfMatches(String regex, TextView view, Set uniqueTextViews){ 117 | if(view == null) { 118 | return uniqueTextViews.size(); 119 | } 120 | 121 | Pattern pattern = null; 122 | try{ 123 | pattern = Pattern.compile(regex); 124 | }catch(PatternSyntaxException e){ 125 | pattern = Pattern.compile(regex, Pattern.LITERAL); 126 | } 127 | 128 | Matcher matcher = pattern.matcher(view.getText().toString()); 129 | 130 | if (matcher.find()){ 131 | uniqueTextViews.add(view); 132 | } 133 | 134 | if (view.getError() != null){ 135 | matcher = pattern.matcher(view.getError().toString()); 136 | if (matcher.find()){ 137 | uniqueTextViews.add(view); 138 | } 139 | } 140 | if (view.getText().toString().equals("") && view.getHint() != null){ 141 | matcher = pattern.matcher(view.getHint().toString()); 142 | if (matcher.find()){ 143 | uniqueTextViews.add(view); 144 | } 145 | } 146 | return uniqueTextViews.size(); 147 | } 148 | 149 | /** 150 | * Filters a collection of Views and returns a list that contains only Views 151 | * with text that matches a specified regular expression. 152 | * 153 | * @param views The collection of views to scan 154 | * @param regex The text pattern to search for 155 | * @return A list of views whose text matches the given regex 156 | */ 157 | 158 | public static List filterViewsByText(Iterable views, String regex) { 159 | return filterViewsByText(views, Pattern.compile(regex)); 160 | } 161 | 162 | /** 163 | * Filters a collection of Views and returns a list that contains only Views 164 | * with text that matches a specified regular expression. 165 | * 166 | * @param views The collection of views to scan 167 | * @param regex The text pattern to search for 168 | * @return A list of views whose text matches the given regex 169 | */ 170 | 171 | public static List filterViewsByText(Iterable views, Pattern regex) { 172 | final ArrayList filteredViews = new ArrayList(); 173 | for (T view : views) { 174 | if (view != null && regex.matcher(view.getText()).matches()) { 175 | filteredViews.add(view); 176 | } 177 | } 178 | return filteredViews; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/RobotiumWeb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used by the web methods. 3 | * 4 | * @author Renas Reda, renas.reda@robotium.com 5 | * 6 | */ 7 | 8 | function allWebElements() { 9 | for (var key in document.all){ 10 | try{ 11 | promptElement(document.all[key]); 12 | }catch(ignored){} 13 | } 14 | finished(); 15 | } 16 | 17 | function allTexts() { 18 | var range = document.createRange(); 19 | var walk=document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); 20 | while(n=walk.nextNode()){ 21 | try{ 22 | promptText(n, range); 23 | }catch(ignored){} 24 | } 25 | finished(); 26 | } 27 | 28 | function clickElement(element){ 29 | var e = document.createEvent('MouseEvents'); 30 | e.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); 31 | element.dispatchEvent(e); 32 | } 33 | 34 | function id(id, click) { 35 | var element = document.getElementById(id); 36 | if(element != null){ 37 | 38 | if(click == 'true'){ 39 | clickElement(element); 40 | } 41 | else{ 42 | promptElement(element); 43 | } 44 | } 45 | else { 46 | for (var key in document.all){ 47 | try{ 48 | element = document.all[key]; 49 | if(element.id == id) { 50 | if(click == 'true'){ 51 | clickElement(element); 52 | return; 53 | } 54 | else{ 55 | promptElement(element); 56 | } 57 | } 58 | } catch(ignored){} 59 | } 60 | } 61 | finished(); 62 | } 63 | 64 | function xpath(xpath, click) { 65 | var elements = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 66 | 67 | if (elements){ 68 | var element = elements.iterateNext(); 69 | while(element) { 70 | if(click == 'true'){ 71 | clickElement(element); 72 | return; 73 | } 74 | else{ 75 | promptElement(element); 76 | element = elements.iterateNext(); 77 | } 78 | } 79 | finished(); 80 | } 81 | } 82 | 83 | function cssSelector(cssSelector, click) { 84 | var elements = document.querySelectorAll(cssSelector); 85 | for (var key in elements) { 86 | if(elements != null){ 87 | try{ 88 | if(click == 'true'){ 89 | clickElement(elements[key]); 90 | return; 91 | } 92 | else{ 93 | promptElement(elements[key]); 94 | } 95 | }catch(ignored){} 96 | } 97 | } 98 | finished(); 99 | } 100 | 101 | function name(name, click) { 102 | var walk=document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); 103 | while(n=walk.nextNode()){ 104 | try{ 105 | var attributeName = n.getAttribute('name'); 106 | if(attributeName != null && attributeName.trim().length>0 && attributeName == name){ 107 | if(click == 'true'){ 108 | clickElement(n); 109 | return; 110 | } 111 | else{ 112 | promptElement(n); 113 | } 114 | } 115 | }catch(ignored){} 116 | } 117 | finished(); 118 | } 119 | 120 | function className(nameOfClass, click) { 121 | var walk=document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); 122 | while(n=walk.nextNode()){ 123 | try{ 124 | var className = n.className; 125 | if(className != null && className.trim().length>0 && className == nameOfClass) { 126 | if(click == 'true'){ 127 | clickElement(n); 128 | return; 129 | } 130 | else{ 131 | promptElement(n); 132 | } 133 | } 134 | }catch(ignored){} 135 | } 136 | finished(); 137 | } 138 | 139 | function textContent(text, click) { 140 | var range = document.createRange(); 141 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,null,false); 142 | while(n=walk.nextNode()){ 143 | try{ 144 | var textContent = n.textContent; 145 | if(textContent.trim() == text.trim()){ 146 | if(click == 'true'){ 147 | clickElement(n); 148 | return; 149 | } 150 | else{ 151 | promptText(n, range); 152 | } 153 | } 154 | }catch(ignored){} 155 | } 156 | finished(); 157 | } 158 | 159 | function tagName(tagName, click) { 160 | var elements = document.getElementsByTagName(tagName); 161 | for (var key in elements) { 162 | if(elements != null){ 163 | try{ 164 | if(click == 'true'){ 165 | clickElement(elements[key]); 166 | return; 167 | } 168 | else{ 169 | promptElement(elements[key]); 170 | } 171 | }catch(ignored){} 172 | } 173 | } 174 | finished(); 175 | } 176 | 177 | function enterTextById(id, text) { 178 | var element = document.getElementById(id); 179 | if(element != null) 180 | element.value = text; 181 | 182 | finished(); 183 | } 184 | 185 | function enterTextByXpath(xpath, text) { 186 | var element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; 187 | if(element != null) 188 | element.value = text; 189 | 190 | finished(); 191 | } 192 | 193 | function enterTextByCssSelector(cssSelector, text) { 194 | var element = document.querySelector(cssSelector); 195 | if(element != null) 196 | element.value = text; 197 | 198 | finished(); 199 | } 200 | 201 | function enterTextByName(name, text) { 202 | var walk=document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); 203 | while(n=walk.nextNode()){ 204 | var attributeName = n.getAttribute('name'); 205 | if(attributeName != null && attributeName.trim().length>0 && attributeName == name) 206 | n.value=text; 207 | } 208 | finished(); 209 | } 210 | 211 | function enterTextByClassName(name, text) { 212 | var walk=document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false); 213 | while(n=walk.nextNode()){ 214 | var className = n.className; 215 | if(className != null && className.trim().length>0 && className == name) 216 | n.value=text; 217 | } 218 | finished(); 219 | } 220 | 221 | function enterTextByTextContent(textContent, text) { 222 | var walk=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT, null, false); 223 | while(n=walk.nextNode()){ 224 | var textValue = n.textContent; 225 | if(textValue == textContent) 226 | n.parentNode.value = text; 227 | } 228 | finished(); 229 | } 230 | 231 | function enterTextByTagName(tagName, text) { 232 | var elements = document.getElementsByTagName(tagName); 233 | if(elements != null){ 234 | elements[0].value = text; 235 | } 236 | finished(); 237 | } 238 | 239 | function promptElement(element) { 240 | var id = element.id; 241 | var text = element.innerText; 242 | if(text.trim().length == 0){ 243 | text = element.value; 244 | } 245 | var name = element.getAttribute('name'); 246 | var className = element.className; 247 | var tagName = element.tagName; 248 | var attributes = ""; 249 | var htmlAttributes = element.attributes; 250 | for (var i = 0, htmlAttribute; htmlAttribute = htmlAttributes[i]; i++){ 251 | attributes += htmlAttribute.name + "::" + htmlAttribute.value; 252 | if (i + 1 < htmlAttributes.length) { 253 | attributes += "#$"; 254 | } 255 | } 256 | 257 | var rect = element.getBoundingClientRect(); 258 | if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){ 259 | prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height + ';,' + attributes); 260 | } 261 | } 262 | 263 | function promptText(element, range) { 264 | var text = element.textContent; 265 | if(text.trim().length>0) { 266 | range.selectNodeContents(element); 267 | var rect = range.getBoundingClientRect(); 268 | if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){ 269 | var id = element.parentNode.id; 270 | var name = element.parentNode.getAttribute('name'); 271 | var className = element.parentNode.className; 272 | var tagName = element.parentNode.tagName; 273 | prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height); 274 | } 275 | } 276 | } 277 | 278 | function finished(){ 279 | prompt('robotium-finished'); 280 | } 281 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/RobotiumWebClient.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.List; 4 | import android.app.Instrumentation; 5 | import android.graphics.Bitmap; 6 | import android.os.Message; 7 | import android.view.View; 8 | import android.webkit.ConsoleMessage; 9 | import android.webkit.GeolocationPermissions; 10 | import android.webkit.JsPromptResult; 11 | import android.webkit.JsResult; 12 | import android.webkit.ValueCallback; 13 | import android.webkit.WebChromeClient; 14 | import android.webkit.WebStorage; 15 | import android.webkit.WebView; 16 | 17 | /** 18 | * WebChromeClient used to get information on web elements by injections of JavaScript. 19 | * 20 | * @author Renas Reda, renas.reda@robotium.com 21 | * 22 | */ 23 | 24 | class RobotiumWebClient extends WebChromeClient{ 25 | WebElementCreator webElementCreator; 26 | private Instrumentation inst; 27 | private WebChromeClient robotiumWebClient; 28 | private WebChromeClient originalWebChromeClient = null; 29 | 30 | 31 | /** 32 | * Constructs this object. 33 | * 34 | * @param instrumentation the {@code Instrumentation} instance 35 | * @param webElementCreator the {@code WebElementCreator} instance 36 | */ 37 | 38 | public RobotiumWebClient(Instrumentation inst, WebElementCreator webElementCreator){ 39 | this.inst = inst; 40 | this.webElementCreator = webElementCreator; 41 | robotiumWebClient = this; 42 | } 43 | 44 | /** 45 | * Enables JavaScript in the given {@code WebViews} objects. 46 | * 47 | * @param webViews the {@code WebView} objects to enable JavaScript in 48 | */ 49 | 50 | public void enableJavascriptAndSetRobotiumWebClient(List webViews, WebChromeClient originalWebChromeClient){ 51 | this.originalWebChromeClient = originalWebChromeClient; 52 | for(final WebView webView : webViews){ 53 | if(webView != null){ 54 | inst.runOnMainSync(new Runnable() { 55 | public void run() { 56 | webView.getSettings().setJavaScriptEnabled(true); 57 | webView.setWebChromeClient(robotiumWebClient); 58 | } 59 | }); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Overrides onJsPrompt in order to create {@code WebElement} objects based on the web elements attributes prompted by the injections of JavaScript 66 | */ 67 | 68 | @Override 69 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) { 70 | 71 | if(message != null && (message.contains(";,") || message.contains("robotium-finished"))){ 72 | 73 | if(message.equals("robotium-finished")){ 74 | webElementCreator.setFinished(true); 75 | } 76 | else{ 77 | webElementCreator.createWebElementAndAddInList(message, view); 78 | } 79 | r.confirm(); 80 | return true; 81 | } 82 | else { 83 | if(originalWebChromeClient != null) { 84 | return originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, r); 85 | } 86 | return true; 87 | } 88 | 89 | } 90 | 91 | @Override 92 | public Bitmap getDefaultVideoPoster() { 93 | if (originalWebChromeClient != null) { 94 | return originalWebChromeClient.getDefaultVideoPoster(); 95 | } 96 | return null; 97 | } 98 | 99 | @Override 100 | public View getVideoLoadingProgressView() { 101 | if (originalWebChromeClient != null) { 102 | return originalWebChromeClient.getVideoLoadingProgressView(); 103 | } 104 | return null; 105 | } 106 | 107 | @Override 108 | public void getVisitedHistory(ValueCallback callback) { 109 | if (originalWebChromeClient != null) { 110 | originalWebChromeClient.getVisitedHistory(callback); 111 | } 112 | } 113 | 114 | @Override 115 | public void onCloseWindow(WebView window) { 116 | if (originalWebChromeClient != null) { 117 | originalWebChromeClient.onCloseWindow(window); 118 | } 119 | } 120 | 121 | @Override 122 | public void onConsoleMessage(String message, int lineNumber, String sourceID) { 123 | if (originalWebChromeClient != null) { 124 | originalWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); 125 | } 126 | } 127 | 128 | @Override 129 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 130 | if (originalWebChromeClient != null) { 131 | return originalWebChromeClient.onConsoleMessage(consoleMessage); 132 | } 133 | return true; 134 | } 135 | 136 | @Override 137 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { 138 | if (originalWebChromeClient != null) { 139 | return originalWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 140 | } 141 | return true; 142 | } 143 | 144 | @Override 145 | public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, 146 | long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) { 147 | if (originalWebChromeClient != null) { 148 | originalWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 149 | } 150 | } 151 | 152 | @Override 153 | public void onGeolocationPermissionsHidePrompt() { 154 | if (originalWebChromeClient != null) { 155 | originalWebChromeClient.onGeolocationPermissionsHidePrompt(); 156 | } 157 | } 158 | 159 | @Override 160 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 161 | if (originalWebChromeClient != null) { 162 | originalWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); 163 | } 164 | } 165 | 166 | @Override 167 | public void onHideCustomView() { 168 | if (originalWebChromeClient != null) { 169 | originalWebChromeClient.onHideCustomView(); 170 | } 171 | } 172 | 173 | @Override 174 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 175 | if (originalWebChromeClient != null) { 176 | return originalWebChromeClient.onJsAlert(view, url, message, result); 177 | } 178 | return true; 179 | } 180 | 181 | @Override 182 | public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) { 183 | if (originalWebChromeClient.onJsBeforeUnload(view, url, message, result)) { 184 | return originalWebChromeClient.onJsBeforeUnload(view, url, message, result); 185 | } 186 | return true; 187 | } 188 | 189 | @Override 190 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 191 | if (originalWebChromeClient != null) { 192 | return originalWebChromeClient.onJsConfirm(view, url, message, result); 193 | } 194 | return true; 195 | } 196 | 197 | @Override 198 | public boolean onJsTimeout() { 199 | if (originalWebChromeClient != null) { 200 | return originalWebChromeClient.onJsTimeout(); 201 | } 202 | return true; 203 | } 204 | 205 | @Override 206 | public void onProgressChanged(WebView view, int newProgress) { 207 | if (originalWebChromeClient != null) { 208 | originalWebChromeClient.onProgressChanged(view, newProgress); 209 | } 210 | } 211 | 212 | @Override 213 | public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) { 214 | if (originalWebChromeClient != null) { 215 | originalWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 216 | } 217 | } 218 | 219 | @Override 220 | public void onReceivedIcon(WebView view, Bitmap icon) { 221 | if (originalWebChromeClient != null) { 222 | originalWebChromeClient.onReceivedIcon(view, icon); 223 | } 224 | } 225 | 226 | @Override 227 | public void onReceivedTitle(WebView view, String title) { 228 | if (originalWebChromeClient != null) { 229 | originalWebChromeClient.onReceivedTitle(view, title); 230 | } 231 | } 232 | 233 | @Override 234 | public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) { 235 | if (originalWebChromeClient != null) { 236 | originalWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); 237 | } 238 | } 239 | 240 | @Override 241 | public void onRequestFocus(WebView view) { 242 | if (originalWebChromeClient != null) { 243 | originalWebChromeClient.onRequestFocus(view); 244 | } 245 | } 246 | 247 | @Override 248 | public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { 249 | if (originalWebChromeClient != null) { 250 | originalWebChromeClient.onShowCustomView(view, callback); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Rotator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.InputDevice; 7 | import android.view.MotionEvent; 8 | import android.view.MotionEvent.PointerCoords; 9 | import android.view.MotionEvent.PointerProperties; 10 | 11 | class Rotator 12 | { 13 | private final Instrumentation _instrument; 14 | private static final int EVENT_TIME_INTERVAL_MS = 10; 15 | public static final int LARGE = 0; 16 | public static final int SMALL = 1; 17 | 18 | public Rotator(Instrumentation inst) 19 | { 20 | this._instrument = inst; 21 | } 22 | 23 | public void generateRotateGesture(int size, PointF center1, PointF center2) 24 | { 25 | double incrementFactor = 0; 26 | float startX1 = center1.x; 27 | float startY1 = center1.y; 28 | float startX2 = center2.x; 29 | float startY2 = center2.y; 30 | 31 | long downTime = SystemClock.uptimeMillis(); 32 | long eventTime = SystemClock.uptimeMillis(); 33 | 34 | // pointer 1 35 | float x1 = startX1; 36 | float y1 = startY1; 37 | 38 | // pointer 2 39 | float x2 = startX2; 40 | float y2 = startY2; 41 | 42 | PointerCoords[] pointerCoords = new PointerCoords[2]; 43 | PointerCoords pc1 = new PointerCoords(); 44 | PointerCoords pc2 = new PointerCoords(); 45 | pc1.x = x1; 46 | pc1.y = y1; 47 | pc1.pressure = 1; 48 | pc1.size = 1; 49 | pc2.x = x2; 50 | pc2.y = y2; 51 | pc2.pressure = 1; 52 | pc2.size = 1; 53 | pointerCoords[0] = pc1; 54 | pointerCoords[1] = pc2; 55 | 56 | PointerProperties[] pointerProperties = new PointerProperties[2]; 57 | PointerProperties pp1 = new PointerProperties(); 58 | PointerProperties pp2 = new PointerProperties(); 59 | pp1.id = 0; 60 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 61 | pp2.id = 1; 62 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 63 | pointerProperties[0] = pp1; 64 | pointerProperties[1] = pp2; 65 | 66 | MotionEvent event; 67 | // send the initial touches 68 | event = MotionEvent.obtain(downTime, eventTime, 69 | MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords, 70 | 0, 0, // metaState, buttonState 71 | 1, // x precision 72 | 1, // y precision 73 | 0, 0, // deviceId, edgeFlags 74 | InputDevice.SOURCE_TOUCHSCREEN, 0); // source, flags 75 | _instrument.sendPointerSync(event); 76 | 77 | event = MotionEvent.obtain(downTime, eventTime, 78 | MotionEvent.ACTION_POINTER_DOWN 79 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 80 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 81 | InputDevice.SOURCE_TOUCHSCREEN, 0); 82 | _instrument.sendPointerSync(event); 83 | 84 | switch(size) 85 | { 86 | case 0: 87 | { 88 | incrementFactor = 0.01; 89 | } 90 | break; 91 | case 1: 92 | { 93 | incrementFactor = 0.1; 94 | } 95 | break; 96 | } 97 | for (double i = 0; i < Math.PI; i += incrementFactor) 98 | { 99 | eventTime += EVENT_TIME_INTERVAL_MS; 100 | pointerCoords[0].x += Math.cos(i); 101 | pointerCoords[0].y += Math.sin(i); 102 | pointerCoords[1].x += Math.cos(i + Math.PI); 103 | pointerCoords[1].y += Math.sin(i + Math.PI); 104 | 105 | event = MotionEvent.obtain(downTime, eventTime, 106 | MotionEvent.ACTION_MOVE, 2, pointerProperties, 107 | pointerCoords, 0, 0, 1, 1, 0, 0, 108 | InputDevice.SOURCE_TOUCHSCREEN, 0); 109 | _instrument.sendPointerSync(event); 110 | } 111 | 112 | // and remove them fingers from the screen 113 | eventTime += EVENT_TIME_INTERVAL_MS; 114 | event = MotionEvent.obtain(downTime, eventTime, 115 | MotionEvent.ACTION_POINTER_UP 116 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 117 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 118 | InputDevice.SOURCE_TOUCHSCREEN, 0); 119 | _instrument.sendPointerSync(event); 120 | 121 | eventTime += EVENT_TIME_INTERVAL_MS; 122 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, 123 | 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 124 | InputDevice.SOURCE_TOUCHSCREEN, 0); 125 | _instrument.sendPointerSync(event); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/ScreenshotTaker.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.text.SimpleDateFormat; 6 | import java.util.ArrayList; 7 | import java.util.Date; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.TimeUnit; 10 | import com.robotium.solo.Solo.Config; 11 | import com.robotium.solo.Solo.Config.ScreenshotFileType; 12 | import android.app.Activity; 13 | import android.app.Instrumentation; 14 | import android.graphics.Bitmap; 15 | import android.graphics.Canvas; 16 | import android.graphics.Picture; 17 | import android.opengl.GLSurfaceView; 18 | import android.opengl.GLSurfaceView.Renderer; 19 | import android.os.Handler; 20 | import android.os.HandlerThread; 21 | import android.os.Message; 22 | import android.os.SystemClock; 23 | import android.util.Log; 24 | import android.view.View; 25 | import android.webkit.WebView; 26 | 27 | /** 28 | * Contains screenshot methods like: takeScreenshot(final View, final String name), startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames), 29 | * stopScreenshotSequence(). 30 | * 31 | * 32 | * @author Renas Reda, renas.reda@robotium.com 33 | * 34 | */ 35 | 36 | class ScreenshotTaker { 37 | 38 | private static final long TIMEOUT_SCREENSHOT_MUTEX = TimeUnit.SECONDS.toMillis(2); 39 | private final Object screenshotMutex = new Object(); 40 | private final Config config; 41 | private final Instrumentation instrumentation; 42 | private final ActivityUtils activityUtils; 43 | private final String LOG_TAG = "Robotium"; 44 | private ScreenshotSequenceThread screenshotSequenceThread = null; 45 | private HandlerThread screenShotSaverThread = null; 46 | private ScreenShotSaver screenShotSaver = null; 47 | private final ViewFetcher viewFetcher; 48 | private final Sleeper sleeper; 49 | 50 | 51 | /** 52 | * Constructs this object. 53 | * 54 | * @param config the {@code Config} instance 55 | * @param instrumentation the {@code Instrumentation} instance. 56 | * @param activityUtils the {@code ActivityUtils} instance 57 | * @param viewFetcher the {@code ViewFetcher} instance 58 | * @param sleeper the {@code Sleeper} instance 59 | * 60 | */ 61 | ScreenshotTaker(Config config, Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Sleeper sleeper) { 62 | this.config = config; 63 | this.instrumentation = instrumentation; 64 | this.activityUtils = activityUtils; 65 | this.viewFetcher = viewFetcher; 66 | this.sleeper = sleeper; 67 | } 68 | 69 | /** 70 | * Takes a screenshot and saves it in the {@link Config} objects save path. 71 | * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test. 72 | * 73 | * @param name the name to give the screenshot image 74 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 75 | */ 76 | public void takeScreenshot(final String name, final int quality) { 77 | View decorView = getScreenshotView(); 78 | if(decorView == null) 79 | return; 80 | 81 | initScreenShotSaver(); 82 | ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality); 83 | 84 | synchronized (screenshotMutex) { 85 | Activity activity = activityUtils.getCurrentActivity(false); 86 | if(activity != null) 87 | activity.runOnUiThread(runnable); 88 | else 89 | instrumentation.runOnMainSync(runnable); 90 | 91 | try { 92 | screenshotMutex.wait(TIMEOUT_SCREENSHOT_MUTEX); 93 | } catch (InterruptedException ignored) { 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Takes a screenshot sequence and saves the images with the name prefix in the {@link Config} objects save path. 100 | * 101 | * The name prefix is appended with "_" + sequence_number for each image in the sequence, 102 | * where numbering starts at 0. 103 | * 104 | * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in the 105 | * AndroidManifest.xml of the application under test. 106 | * 107 | * Taking a screenshot will take on the order of 40-100 milliseconds of time on the 108 | * main UI thread. Therefore it is possible to mess up the timing of tests if 109 | * the frameDelay value is set too small. 110 | * 111 | * At present multiple simultaneous screenshot sequences are not supported. 112 | * This method will throw an exception if stopScreenshotSequence() has not been 113 | * called to finish any prior sequences. 114 | * 115 | * @param name the name prefix to give the screenshot 116 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality) 117 | * @param frameDelay the time in milliseconds to wait between each frame 118 | * @param maxFrames the maximum number of frames that will comprise this sequence 119 | * 120 | */ 121 | public void startScreenshotSequence(final String name, final int quality, final int frameDelay, final int maxFrames) { 122 | initScreenShotSaver(); 123 | 124 | if(screenshotSequenceThread != null) { 125 | throw new RuntimeException("only one screenshot sequence is supported at a time"); 126 | } 127 | 128 | screenshotSequenceThread = new ScreenshotSequenceThread(name, quality, frameDelay, maxFrames); 129 | 130 | screenshotSequenceThread.start(); 131 | } 132 | 133 | /** 134 | * Causes a screenshot sequence to end. 135 | * 136 | * If this method is not called to end a sequence and a prior sequence is still in 137 | * progress, startScreenshotSequence() will throw an exception. 138 | */ 139 | public void stopScreenshotSequence() { 140 | if(screenshotSequenceThread != null) { 141 | screenshotSequenceThread.interrupt(); 142 | screenshotSequenceThread = null; 143 | } 144 | } 145 | 146 | /** 147 | * Gets the proper view to use for a screenshot. 148 | */ 149 | private View getScreenshotView() { 150 | View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); 151 | final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout(); 152 | 153 | while (decorView == null) { 154 | 155 | final boolean timedOut = SystemClock.uptimeMillis() > endTime; 156 | 157 | if (timedOut){ 158 | return null; 159 | } 160 | sleeper.sleepMini(); 161 | decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); 162 | } 163 | wrapAllGLViews(decorView); 164 | 165 | return decorView; 166 | } 167 | 168 | /** 169 | * Extract and wrap the all OpenGL ES Renderer. 170 | */ 171 | private void wrapAllGLViews(View decorView) { 172 | ArrayList currentViews = viewFetcher.getCurrentViews(GLSurfaceView.class, true, decorView); 173 | final CountDownLatch latch = new CountDownLatch(currentViews.size()); 174 | 175 | for (GLSurfaceView glView : currentViews) { 176 | Object renderContainer = new Reflect(glView).field("mGLThread") 177 | .type(GLSurfaceView.class).out(Object.class); 178 | 179 | Renderer renderer = new Reflect(renderContainer).field("mRenderer").out(Renderer.class); 180 | 181 | if (renderer == null) { 182 | renderer = new Reflect(glView).field("mRenderer").out(Renderer.class); 183 | renderContainer = glView; 184 | } 185 | if (renderer == null) { 186 | latch.countDown(); 187 | continue; 188 | } 189 | if (renderer instanceof GLRenderWrapper) { 190 | GLRenderWrapper wrapper = (GLRenderWrapper) renderer; 191 | wrapper.setTakeScreenshot(); 192 | wrapper.setLatch(latch); 193 | } else { 194 | GLRenderWrapper wrapper = new GLRenderWrapper(glView, renderer, latch); 195 | new Reflect(renderContainer).field("mRenderer").in(wrapper); 196 | } 197 | } 198 | 199 | try { 200 | latch.await(); 201 | } catch (InterruptedException ex) { 202 | ex.printStackTrace(); 203 | } 204 | } 205 | 206 | 207 | /** 208 | * Returns a bitmap of a given WebView. 209 | * 210 | * @param webView the webView to save a bitmap from 211 | * @return a bitmap of the given web view 212 | * 213 | */ 214 | 215 | private Bitmap getBitmapOfWebView(final WebView webView){ 216 | Picture picture = webView.capturePicture(); 217 | Bitmap b = Bitmap.createBitmap( picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888); 218 | Canvas c = new Canvas(b); 219 | picture.draw(c); 220 | return b; 221 | } 222 | 223 | /** 224 | * Returns a bitmap of a given View. 225 | * 226 | * @param view the view to save a bitmap from 227 | * @return a bitmap of the given view 228 | * 229 | */ 230 | 231 | private Bitmap getBitmapOfView(final View view){ 232 | view.destroyDrawingCache(); 233 | view.buildDrawingCache(false); 234 | Bitmap orig = view.getDrawingCache(); 235 | Bitmap.Config config = null; 236 | 237 | if(orig == null) { 238 | return null; 239 | } 240 | 241 | config = orig.getConfig(); 242 | 243 | if(config == null) { 244 | config = Bitmap.Config.ARGB_8888; 245 | } 246 | Bitmap b = orig.copy(config, false); 247 | orig.recycle(); 248 | view.destroyDrawingCache(); 249 | return b; 250 | } 251 | 252 | /** 253 | * Returns a proper filename depending on if name is given or not. 254 | * 255 | * @param name the given name 256 | * @return a proper filename depedning on if a name is given or not 257 | * 258 | */ 259 | 260 | private String getFileName(final String name){ 261 | SimpleDateFormat sdf = new SimpleDateFormat("ddMMyy-hhmmss"); 262 | String fileName = null; 263 | if(name == null){ 264 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 265 | fileName = sdf.format( new Date()).toString()+ ".jpg"; 266 | } 267 | else{ 268 | fileName = sdf.format( new Date()).toString()+ ".png"; 269 | } 270 | } 271 | else { 272 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 273 | fileName = name + ".jpg"; 274 | } 275 | else { 276 | fileName = name + ".png"; 277 | } 278 | } 279 | return fileName; 280 | } 281 | 282 | /** 283 | * This method initializes the aysnc screenshot saving logic 284 | */ 285 | private void initScreenShotSaver() { 286 | if(screenShotSaverThread == null || screenShotSaver == null) { 287 | screenShotSaverThread = new HandlerThread("ScreenShotSaver"); 288 | screenShotSaverThread.start(); 289 | screenShotSaver = new ScreenShotSaver(screenShotSaverThread); 290 | } 291 | } 292 | 293 | /** 294 | * This is the thread which causes a screenshot sequence to happen 295 | * in parallel with testing. 296 | */ 297 | private class ScreenshotSequenceThread extends Thread { 298 | private int seqno = 0; 299 | 300 | private String name; 301 | private int quality; 302 | private int frameDelay; 303 | private int maxFrames; 304 | 305 | private boolean keepRunning = true; 306 | 307 | public ScreenshotSequenceThread(String _name, int _quality, int _frameDelay, int _maxFrames) { 308 | name = _name; 309 | quality = _quality; 310 | frameDelay = _frameDelay; 311 | maxFrames = _maxFrames; 312 | } 313 | 314 | public void run() { 315 | while(seqno < maxFrames) { 316 | if(!keepRunning || Thread.interrupted()) break; 317 | doScreenshot(); 318 | seqno++; 319 | try { 320 | Thread.sleep(frameDelay); 321 | } catch (InterruptedException e) { 322 | } 323 | } 324 | screenshotSequenceThread = null; 325 | } 326 | 327 | public void doScreenshot() { 328 | View v = getScreenshotView(); 329 | if(v == null) keepRunning = false; 330 | String final_name = name+"_"+seqno; 331 | ScreenshotRunnable r = new ScreenshotRunnable(v, final_name, quality); 332 | Log.d(LOG_TAG, "taking screenshot "+final_name); 333 | Activity activity = activityUtils.getCurrentActivity(false); 334 | if(activity != null){ 335 | activity.runOnUiThread(r); 336 | } 337 | else { 338 | instrumentation.runOnMainSync(r); 339 | } 340 | } 341 | 342 | public void interrupt() { 343 | keepRunning = false; 344 | super.interrupt(); 345 | } 346 | } 347 | 348 | /** 349 | * Here we have a Runnable which is responsible for taking the actual screenshot, 350 | * and then posting the bitmap to a Handler which will save it. 351 | * 352 | * This Runnable is run on the UI thread. 353 | */ 354 | private class ScreenshotRunnable implements Runnable { 355 | 356 | private View view; 357 | private String name; 358 | private int quality; 359 | 360 | public ScreenshotRunnable(final View _view, final String _name, final int _quality) { 361 | view = _view; 362 | name = _name; 363 | quality = _quality; 364 | } 365 | 366 | public void run() { 367 | if(view !=null){ 368 | Bitmap b; 369 | 370 | if(view instanceof WebView){ 371 | b = getBitmapOfWebView((WebView) view); 372 | } 373 | else{ 374 | b = getBitmapOfView(view); 375 | } 376 | if(b != null) { 377 | screenShotSaver.saveBitmap(b, name, quality); 378 | b = null; 379 | // Return here so that the screenshotMutex is not unlocked, 380 | // since this is handled by save bitmap 381 | return; 382 | } 383 | else 384 | Log.d(LOG_TAG, "NULL BITMAP!!"); 385 | } 386 | 387 | // Make sure the screenshotMutex is unlocked 388 | synchronized (screenshotMutex) { 389 | screenshotMutex.notify(); 390 | } 391 | } 392 | } 393 | 394 | /** 395 | * This class is a Handler which deals with saving the screenshots on a separate thread. 396 | * 397 | * The screenshot logic by necessity has to run on the ui thread. However, in practice 398 | * it seems that saving a screenshot (with quality 100) takes approx twice as long 399 | * as taking it in the first place. 400 | * 401 | * Saving the screenshots in a separate thread like this will thus make the screenshot 402 | * process approx 3x faster as far as the main thread is concerned. 403 | * 404 | */ 405 | private class ScreenShotSaver extends Handler { 406 | public ScreenShotSaver(HandlerThread thread) { 407 | super(thread.getLooper()); 408 | } 409 | 410 | /** 411 | * This method posts a Bitmap with meta-data to the Handler queue. 412 | * 413 | * @param bitmap the bitmap to save 414 | * @param name the name of the file 415 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 416 | */ 417 | public void saveBitmap(Bitmap bitmap, String name, int quality) { 418 | Message message = this.obtainMessage(); 419 | message.arg1 = quality; 420 | message.obj = bitmap; 421 | message.getData().putString("name", name); 422 | this.sendMessage(message); 423 | } 424 | 425 | /** 426 | * Here we process the Handler queue and save the bitmaps. 427 | * 428 | * @param message A Message containing the bitmap to save, and some metadata. 429 | */ 430 | public void handleMessage(Message message) { 431 | synchronized (screenshotMutex) { 432 | String name = message.getData().getString("name"); 433 | int quality = message.arg1; 434 | Bitmap b = (Bitmap)message.obj; 435 | if(b != null) { 436 | saveFile(name, b, quality); 437 | b.recycle(); 438 | } 439 | else { 440 | Log.d(LOG_TAG, "NULL BITMAP!!"); 441 | } 442 | 443 | screenshotMutex.notify(); 444 | } 445 | } 446 | 447 | /** 448 | * Saves a file. 449 | * 450 | * @param name the name of the file 451 | * @param b the bitmap to save 452 | * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). 453 | * 454 | */ 455 | private void saveFile(String name, Bitmap b, int quality){ 456 | FileOutputStream fos = null; 457 | String fileName = getFileName(name); 458 | 459 | File directory = new File(config.screenshotSavePath); 460 | directory.mkdir(); 461 | 462 | File fileToSave = new File(directory,fileName); 463 | try { 464 | fos = new FileOutputStream(fileToSave); 465 | if(config.screenshotFileType == ScreenshotFileType.JPEG){ 466 | if (b.compress(Bitmap.CompressFormat.JPEG, quality, fos) == false){ 467 | Log.d(LOG_TAG, "Compress/Write failed"); 468 | } 469 | } 470 | else{ 471 | if (b.compress(Bitmap.CompressFormat.PNG, quality, fos) == false){ 472 | Log.d(LOG_TAG, "Compress/Write failed"); 473 | } 474 | } 475 | fos.flush(); 476 | fos.close(); 477 | } catch (Exception e) { 478 | Log.d(LOG_TAG, "Can't save the screenshot! Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test."); 479 | e.printStackTrace(); 480 | } 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Scroller.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Set; 6 | import com.robotium.solo.Solo.Config; 7 | import junit.framework.Assert; 8 | import android.app.Instrumentation; 9 | import android.content.Context; 10 | import android.os.SystemClock; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.WindowManager; 14 | import android.webkit.WebView; 15 | import android.widget.AbsListView; 16 | import android.widget.GridView; 17 | import android.widget.ListView; 18 | import android.widget.ScrollView; 19 | 20 | 21 | /** 22 | * Contains scroll methods. Examples are scrollDown(), scrollUpList(), 23 | * scrollToSide(). 24 | * 25 | * @author Renas Reda, renas.reda@robotium.com 26 | * 27 | */ 28 | 29 | class Scroller { 30 | 31 | public static final int DOWN = 0; 32 | public static final int UP = 1; 33 | public enum Side {LEFT, RIGHT} 34 | private boolean canScroll = false; 35 | private final Instrumentation inst; 36 | private final ViewFetcher viewFetcher; 37 | private final Sleeper sleeper; 38 | private final Config config; 39 | 40 | 41 | /** 42 | * Constructs this object. 43 | * 44 | * @param inst the {@code Instrumentation} instance 45 | * @param viewFetcher the {@code ViewFetcher} instance 46 | * @param sleeper the {@code Sleeper} instance 47 | */ 48 | 49 | public Scroller(Config config, Instrumentation inst, ViewFetcher viewFetcher, Sleeper sleeper) { 50 | this.config = config; 51 | this.inst = inst; 52 | this.viewFetcher = viewFetcher; 53 | this.sleeper = sleeper; 54 | } 55 | 56 | 57 | /** 58 | * Simulate touching a specific location and dragging to a new location. 59 | * 60 | * This method was copied from {@code TouchUtils.java} in the Android Open Source Project, and modified here. 61 | * 62 | * @param fromX X coordinate of the initial touch, in screen coordinates 63 | * @param toX Xcoordinate of the drag destination, in screen coordinates 64 | * @param fromY X coordinate of the initial touch, in screen coordinates 65 | * @param toY Y coordinate of the drag destination, in screen coordinates 66 | * @param stepCount How many move steps to include in the drag 67 | */ 68 | 69 | public void drag(float fromX, float toX, float fromY, float toY, 70 | int stepCount) { 71 | long downTime = SystemClock.uptimeMillis(); 72 | long eventTime = SystemClock.uptimeMillis(); 73 | float y = fromY; 74 | float x = fromX; 75 | float yStep = (toY - fromY) / stepCount; 76 | float xStep = (toX - fromX) / stepCount; 77 | MotionEvent event = MotionEvent.obtain(downTime, eventTime,MotionEvent.ACTION_DOWN, fromX, fromY, 0); 78 | try { 79 | inst.sendPointerSync(event); 80 | } catch (SecurityException ignored) {} 81 | for (int i = 0; i < stepCount; ++i) { 82 | y += yStep; 83 | x += xStep; 84 | eventTime = SystemClock.uptimeMillis(); 85 | event = MotionEvent.obtain(downTime, eventTime,MotionEvent.ACTION_MOVE, x, y, 0); 86 | try { 87 | inst.sendPointerSync(event); 88 | } catch (SecurityException ignored) {} 89 | } 90 | eventTime = SystemClock.uptimeMillis(); 91 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP,toX, toY, 0); 92 | try { 93 | inst.sendPointerSync(event); 94 | } catch (SecurityException ignored) {} 95 | } 96 | 97 | 98 | /** 99 | * Scrolls a ScrollView. 100 | * 101 | * @param direction the direction to be scrolled 102 | * @return {@code true} if scrolling occurred, false if it did not 103 | */ 104 | 105 | public boolean scrollView(final View view, int direction){ 106 | if(view == null){ 107 | return false; 108 | } 109 | 110 | int height = view.getHeight(); 111 | height--; 112 | int scrollTo = -1; 113 | 114 | if (direction == DOWN) { 115 | scrollTo = height; 116 | } 117 | 118 | else if (direction == UP) { 119 | scrollTo = -height; 120 | } 121 | 122 | int originalY = view.getScrollY(); 123 | final int scrollAmount = scrollTo; 124 | inst.runOnMainSync(new Runnable(){ 125 | public void run(){ 126 | view.scrollBy(0, scrollAmount); 127 | } 128 | }); 129 | 130 | if (originalY == view.getScrollY()) { 131 | return false; 132 | } 133 | else{ 134 | return true; 135 | } 136 | } 137 | 138 | /** 139 | * Scrolls a ScrollView to top or bottom. 140 | * 141 | * @param direction the direction to be scrolled 142 | */ 143 | 144 | public void scrollViewAllTheWay(final View view, final int direction) { 145 | while(scrollView(view, direction)); 146 | } 147 | 148 | /** 149 | * Scrolls up or down. 150 | * 151 | * @param direction the direction in which to scroll 152 | * @return {@code true} if more scrolling can be done 153 | */ 154 | 155 | public boolean scroll(int direction) { 156 | return scroll(direction, false); 157 | } 158 | 159 | /** 160 | * Scrolls down. 161 | * 162 | * @return {@code true} if more scrolling can be done 163 | */ 164 | 165 | public boolean scrollDown() { 166 | if(!config.shouldScroll) { 167 | return false; 168 | } 169 | return scroll(Scroller.DOWN); 170 | } 171 | 172 | /** 173 | * Scrolls up and down. 174 | * 175 | * @param direction the direction in which to scroll 176 | * @param allTheWay true if the view should be scrolled to the beginning or end, 177 | * false to scroll one page up or down. 178 | * @return {@code true} if more scrolling can be done 179 | */ 180 | 181 | @SuppressWarnings("unchecked") 182 | public boolean scroll(int direction, boolean allTheWay) { 183 | ArrayList viewList = RobotiumUtils.removeInvisibleViews(viewFetcher.getAllViews(true)); 184 | 185 | ArrayList filteredViews = RobotiumUtils.filterViewsToSet(new Class[] { ListView.class, 186 | ScrollView.class, GridView.class, WebView.class}, viewList); 187 | 188 | List scrollableSupportPackageViews = viewFetcher.getScrollableSupportPackageViews(true); 189 | 190 | for(View viewToScroll : scrollableSupportPackageViews){ 191 | filteredViews.add(viewToScroll); 192 | } 193 | 194 | View view = viewFetcher.getFreshestView(filteredViews); 195 | 196 | if (view == null) { 197 | return false; 198 | } 199 | 200 | if (view instanceof AbsListView) { 201 | return scrollList((AbsListView)view, direction, allTheWay); 202 | } 203 | 204 | if(view instanceof WebView){ 205 | return scrollWebView((WebView)view, direction, allTheWay); 206 | } 207 | 208 | if (allTheWay) { 209 | scrollViewAllTheWay(view, direction); 210 | return false; 211 | } else { 212 | return scrollView(view, direction); 213 | } 214 | } 215 | 216 | /** 217 | * Scrolls a WebView. 218 | * 219 | * @param webView the WebView to scroll 220 | * @param direction the direction to scroll 221 | * @param allTheWay {@code true} to scroll the view all the way up or down, {@code false} to scroll one page up or down or down. 222 | * @return {@code true} if more scrolling can be done 223 | */ 224 | 225 | public boolean scrollWebView(final WebView webView, int direction, final boolean allTheWay){ 226 | 227 | if (direction == DOWN) { 228 | inst.runOnMainSync(new Runnable(){ 229 | public void run(){ 230 | canScroll = webView.pageDown(allTheWay); 231 | } 232 | }); 233 | } 234 | if(direction == UP){ 235 | inst.runOnMainSync(new Runnable(){ 236 | public void run(){ 237 | canScroll = webView.pageUp(allTheWay); 238 | } 239 | }); 240 | } 241 | return canScroll; 242 | } 243 | 244 | /** 245 | * Scrolls a list. 246 | * 247 | * @param absListView the list to be scrolled 248 | * @param direction the direction to be scrolled 249 | * @param allTheWay {@code true} to scroll the view all the way up or down, {@code false} to scroll one page up or down 250 | * @return {@code true} if more scrolling can be done 251 | */ 252 | 253 | public boolean scrollList(T absListView, int direction, boolean allTheWay) { 254 | 255 | if(absListView == null){ 256 | return false; 257 | } 258 | 259 | if (direction == DOWN) { 260 | 261 | int listCount = absListView.getCount(); 262 | int lastVisiblePosition = absListView.getLastVisiblePosition(); 263 | 264 | if (allTheWay) { 265 | scrollListToLine(absListView, listCount-1); 266 | return false; 267 | } 268 | 269 | if (lastVisiblePosition >= listCount - 1) { 270 | if(lastVisiblePosition > 0){ 271 | scrollListToLine(absListView, lastVisiblePosition); 272 | } 273 | return false; 274 | } 275 | 276 | int firstVisiblePosition = absListView.getFirstVisiblePosition(); 277 | 278 | 279 | if(firstVisiblePosition != lastVisiblePosition) 280 | scrollListToLine(absListView, lastVisiblePosition); 281 | 282 | else 283 | scrollListToLine(absListView, firstVisiblePosition + 1); 284 | 285 | } else if (direction == UP) { 286 | int firstVisiblePosition = absListView.getFirstVisiblePosition(); 287 | 288 | if (allTheWay || firstVisiblePosition < 2) { 289 | scrollListToLine(absListView, 0); 290 | return false; 291 | } 292 | int lastVisiblePosition = absListView.getLastVisiblePosition(); 293 | 294 | final int lines = lastVisiblePosition - firstVisiblePosition; 295 | 296 | int lineToScrollTo = firstVisiblePosition - lines; 297 | 298 | if(lineToScrollTo == lastVisiblePosition) 299 | lineToScrollTo--; 300 | 301 | if(lineToScrollTo < 0) 302 | lineToScrollTo = 0; 303 | 304 | scrollListToLine(absListView, lineToScrollTo); 305 | } 306 | sleeper.sleep(); 307 | return true; 308 | } 309 | 310 | 311 | /** 312 | * Scroll the list to a given line 313 | * 314 | * @param view the {@link AbsListView} to scroll 315 | * @param line the line to scroll to 316 | */ 317 | 318 | public void scrollListToLine(final T view, final int line){ 319 | if(view == null) 320 | Assert.fail("AbsListView is null!"); 321 | 322 | final int lineToMoveTo; 323 | if(view instanceof GridView) { 324 | lineToMoveTo = line+1; 325 | } 326 | else { 327 | lineToMoveTo = line; 328 | } 329 | 330 | inst.runOnMainSync(new Runnable(){ 331 | public void run(){ 332 | view.setSelection(lineToMoveTo); 333 | } 334 | }); 335 | } 336 | 337 | 338 | /** 339 | * Scrolls horizontally. 340 | * 341 | * @param side the side to which to scroll; {@link Side#RIGHT} or {@link Side#LEFT} 342 | * @param scrollPosition the position to scroll to, from 0 to 1 where 1 is all the way. Example is: 0.55. 343 | * @param stepCount how many move steps to include in the scroll. Less steps results in a faster scroll 344 | */ 345 | 346 | @SuppressWarnings("deprecation") 347 | public void scrollToSide(Side side, float scrollPosition, int stepCount) { 348 | WindowManager windowManager = (WindowManager) 349 | inst.getTargetContext().getSystemService(Context.WINDOW_SERVICE); 350 | 351 | int screenHeight = windowManager.getDefaultDisplay() 352 | .getHeight(); 353 | int screenWidth = windowManager.getDefaultDisplay() 354 | .getWidth(); 355 | float x = screenWidth * scrollPosition; 356 | float y = screenHeight / 2.0f; 357 | if (side == Side.LEFT) 358 | drag(70, x, y, y, stepCount); 359 | else if (side == Side.RIGHT) 360 | drag(x, 0, y, y, stepCount); 361 | } 362 | 363 | /** 364 | * Scrolls view horizontally. 365 | * 366 | * @param view the view to scroll 367 | * @param side the side to which to scroll; {@link Side#RIGHT} or {@link Side#LEFT} 368 | * @param scrollPosition the position to scroll to, from 0 to 1 where 1 is all the way. Example is: 0.55. 369 | * @param stepCount how many move steps to include in the scroll. Less steps results in a faster scroll 370 | */ 371 | 372 | public void scrollViewToSide(View view, Side side, float scrollPosition, int stepCount) { 373 | int[] corners = new int[2]; 374 | view.getLocationOnScreen(corners); 375 | int viewHeight = view.getHeight(); 376 | int viewWidth = view.getWidth(); 377 | float x = corners[0] + viewWidth * scrollPosition; 378 | float y = corners[1] + viewHeight / 2.0f; 379 | if (side == Side.LEFT) 380 | drag(corners[0], x, y, y, stepCount); 381 | else if (side == Side.RIGHT) 382 | drag(x, corners[0], y, y, stepCount); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Searcher.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.concurrent.Callable; 9 | import android.os.SystemClock; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.TextView; 13 | 14 | 15 | /** 16 | * Contains various search methods. Examples are: searchForEditTextWithTimeout(), 17 | * searchForTextWithTimeout(), searchForButtonWithTimeout(). 18 | * 19 | * @author Renas Reda, renas.reda@robotium.com 20 | * 21 | */ 22 | 23 | class Searcher { 24 | 25 | private final ViewFetcher viewFetcher; 26 | private final WebUtils webUtils; 27 | private final Scroller scroller; 28 | private final Sleeper sleeper; 29 | private final String LOG_TAG = "Robotium"; 30 | Set uniqueTextViews; 31 | List webElements; 32 | private int numberOfUniqueViews; 33 | private final int TIMEOUT = 5000; 34 | 35 | 36 | /** 37 | * Constructs this object. 38 | * 39 | * @param viewFetcher the {@code ViewFetcher} instance 40 | * @param webUtils the {@code WebUtils} instance 41 | * @param scroller the {@code Scroller} instance 42 | * @param sleeper the {@code Sleeper} instance. 43 | */ 44 | 45 | public Searcher(ViewFetcher viewFetcher, WebUtils webUtils, Scroller scroller, Sleeper sleeper) { 46 | this.viewFetcher = viewFetcher; 47 | this.webUtils = webUtils; 48 | this.scroller = scroller; 49 | this.sleeper = sleeper; 50 | webElements = new ArrayList(); 51 | uniqueTextViews = new HashSet(); 52 | } 53 | 54 | 55 | /** 56 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 57 | * searched {@code Button} is found a given number of times. Will automatically scroll when needed. 58 | * 59 | * @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class} 60 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 61 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 62 | * matches are expected to be found 63 | * @param scroll whether scrolling should be performed 64 | * @param onlyVisible {@code true} if only texts visible on the screen should be searched 65 | * 66 | * @return {@code true} if a {@code View} of the specified class with the given text is found a given number of 67 | * times, and {@code false} if it is not found 68 | */ 69 | 70 | public boolean searchWithTimeoutFor(Class viewClass, String regex, int expectedMinimumNumberOfMatches, boolean scroll, boolean onlyVisible) { 71 | final long endTime = SystemClock.uptimeMillis() + TIMEOUT; 72 | 73 | TextView foundAnyMatchingView = null; 74 | 75 | while (SystemClock.uptimeMillis() < endTime) { 76 | sleeper.sleep(); 77 | foundAnyMatchingView = searchFor(viewClass, regex, expectedMinimumNumberOfMatches, 0, scroll, onlyVisible); 78 | if (foundAnyMatchingView !=null){ 79 | return true; 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | 86 | /** 87 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 88 | * searched {@code View} is found a given number of times. 89 | * 90 | * @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class} 91 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 92 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 93 | * matches are expected to be found. 94 | * @param timeout the amount of time in milliseconds to wait 95 | * @param scroll whether scrolling should be performed 96 | * @param onlyVisible {@code true} if only texts visible on the screen should be searched 97 | * 98 | * @return {@code true} if a view of the specified class with the given text is found a given number of times. 99 | * {@code false} if it is not found. 100 | */ 101 | 102 | public T searchFor(final Class viewClass, final String regex, int expectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) { 103 | if(expectedMinimumNumberOfMatches < 1) { 104 | expectedMinimumNumberOfMatches = 1; 105 | } 106 | 107 | final Callable> viewFetcherCallback = new Callable>() { 108 | @SuppressWarnings("unchecked") 109 | public Collection call() throws Exception { 110 | sleeper.sleep(); 111 | 112 | ArrayList viewsToReturn = viewFetcher.getCurrentViews(viewClass, true); 113 | 114 | if(onlyVisible){ 115 | viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn); 116 | } 117 | 118 | if(viewClass.isAssignableFrom(TextView.class)) { 119 | viewsToReturn.addAll((Collection) webUtils.getTextViewsFromWebView()); 120 | } 121 | return viewsToReturn; 122 | } 123 | }; 124 | 125 | try { 126 | return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll); 127 | } catch (Exception e) { 128 | throw new RuntimeException(e); 129 | } 130 | } 131 | 132 | /** 133 | * Searches for a view class. 134 | * 135 | * @param uniqueViews the set of unique views 136 | * @param viewClass the view class to search for 137 | * @param index the index of the view class 138 | * @return true if view class if found a given number of times 139 | */ 140 | 141 | public boolean searchFor(Set uniqueViews, Class viewClass, final int index) { 142 | ArrayList allViews = RobotiumUtils.removeInvisibleViews(viewFetcher.getCurrentViews(viewClass, true)); 143 | 144 | int uniqueViewsFound = (getNumberOfUniqueViews(uniqueViews, allViews)); 145 | 146 | if(uniqueViewsFound > 0 && index < uniqueViewsFound) { 147 | return true; 148 | } 149 | 150 | if(uniqueViewsFound > 0 && index == 0) { 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | /** 157 | * Searches for a given view. 158 | * 159 | * @param view the view to search 160 | * @param scroll true if scrolling should be performed 161 | * @return true if view is found 162 | */ 163 | 164 | public boolean searchFor(View view) { 165 | ArrayList views = viewFetcher.getAllViews(true); 166 | for(View v : views){ 167 | if(v.equals(view)){ 168 | return true; 169 | } 170 | } 171 | return false; 172 | } 173 | 174 | /** 175 | * Searches for a {@code View} with the given regex string and returns {@code true} if the 176 | * searched {@code View} is found a given number of times. Will not scroll, because the caller needs to find new 177 | * {@code View}s to evaluate after scrolling, and call this method again. 178 | * 179 | * @param viewFetcherCallback callback which should return an updated collection of views to search 180 | * @param regex the text to search for. The parameter will be interpreted as a regular expression. 181 | * @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more 182 | * matches are expected to be found. 183 | * @param timeout the amount of time in milliseconds to wait 184 | * @param scroll whether scrolling should be performed 185 | * 186 | * @return {@code true} if a view of the specified class with the given text is found a given number of times. 187 | * {@code false} if it is not found. 188 | * 189 | * @throws Exception not really, it's just the signature of {@code Callable} 190 | */ 191 | 192 | public T searchFor(Callable> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception { 193 | final long endTime = SystemClock.uptimeMillis() + timeout; 194 | Collection views; 195 | 196 | while (true) { 197 | final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime; 198 | 199 | if(timedOut){ 200 | logMatchesFound(regex); 201 | return null; 202 | } 203 | 204 | views = viewFetcherCallback.call(); 205 | 206 | for(T view : views){ 207 | if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) { 208 | uniqueTextViews.clear(); 209 | return view; 210 | } 211 | } 212 | if(scroll && !scroller.scrollDown()){ 213 | logMatchesFound(regex); 214 | return null; 215 | } 216 | if(!scroll){ 217 | logMatchesFound(regex); 218 | return null; 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Searches for a web element. 225 | * 226 | * @param by the By object e.g. By.id("id"); 227 | * @param minimumNumberOfMatches the minimum number of matches that are expected to be shown. {@code 0} means any number of matches 228 | * @return the web element or null if not found 229 | */ 230 | 231 | public WebElement searchForWebElement(final By by, int minimumNumberOfMatches){ 232 | 233 | if(minimumNumberOfMatches < 1){ 234 | minimumNumberOfMatches = 1; 235 | } 236 | 237 | List viewsFromScreen = webUtils.getWebElements(by, true); 238 | addViewsToList (webElements, viewsFromScreen); 239 | 240 | return getViewFromList(webElements, minimumNumberOfMatches); 241 | } 242 | 243 | /** 244 | * Adds views to a given list. 245 | * 246 | * @param allWebElements the list of all views 247 | * @param webTextViewsOnScreen the list of views shown on screen 248 | */ 249 | 250 | private void addViewsToList(List allWebElements, List webElementsOnScreen){ 251 | 252 | int[] xyViewFromSet = new int[2]; 253 | int[] xyViewFromScreen = new int[2]; 254 | 255 | for(WebElement textFromScreen : webElementsOnScreen){ 256 | boolean foundView = false; 257 | textFromScreen.getLocationOnScreen(xyViewFromScreen); 258 | 259 | for(WebElement textFromList : allWebElements){ 260 | textFromList.getLocationOnScreen(xyViewFromSet); 261 | 262 | if(textFromScreen.getText().equals(textFromList.getText()) && xyViewFromScreen[0] == xyViewFromSet[0] && xyViewFromScreen[1] == xyViewFromSet[1]) { 263 | foundView = true; 264 | } 265 | } 266 | 267 | if(!foundView){ 268 | allWebElements.add(textFromScreen); 269 | } 270 | } 271 | 272 | } 273 | 274 | /** 275 | * Returns a text view with a given match. 276 | * 277 | * @param webElements the list of views 278 | * @param match the match of the view to return 279 | * @return the view with a given match 280 | */ 281 | 282 | private WebElement getViewFromList(List webElements, int match){ 283 | 284 | WebElement webElementToReturn = null; 285 | 286 | if(webElements.size() >= match){ 287 | 288 | try{ 289 | webElementToReturn = webElements.get(--match); 290 | }catch(Exception ignored){} 291 | } 292 | if(webElementToReturn != null) 293 | webElements.clear(); 294 | 295 | return webElementToReturn; 296 | } 297 | 298 | /** 299 | * Returns the number of unique views. 300 | * 301 | * @param uniqueViews the set of unique views 302 | * @param views the list of all views 303 | * @return number of unique views 304 | */ 305 | 306 | public int getNumberOfUniqueViews(SetuniqueViews, ArrayList views){ 307 | for(int i = 0; i < views.size(); i++){ 308 | uniqueViews.add(views.get(i)); 309 | } 310 | numberOfUniqueViews = uniqueViews.size(); 311 | return numberOfUniqueViews; 312 | } 313 | 314 | /** 315 | * Returns the number of unique views. 316 | * 317 | * @return the number of unique views 318 | */ 319 | 320 | public int getNumberOfUniqueViews(){ 321 | return numberOfUniqueViews; 322 | } 323 | 324 | /** 325 | * Logs a (searchFor failed) message. 326 | * 327 | * @param regex the search string to log 328 | */ 329 | 330 | public void logMatchesFound(String regex){ 331 | if (uniqueTextViews.size() > 0) { 332 | Log.d(LOG_TAG, " There are only " + uniqueTextViews.size() + " matches of '" + regex + "'"); 333 | } 334 | else if(webElements.size() > 0){ 335 | Log.d(LOG_TAG, " There are only " + webElements.size() + " matches of '" + regex + "'"); 336 | } 337 | uniqueTextViews.clear(); 338 | webElements.clear(); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Sender.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | 4 | import junit.framework.Assert; 5 | import android.app.Instrumentation; 6 | import android.view.KeyEvent; 7 | 8 | /** 9 | * Contains send key event methods. Examples are: 10 | * sendKeyCode(), goBack() 11 | * 12 | * @author Renas Reda, renas.reda@robotium.com 13 | * 14 | */ 15 | 16 | class Sender { 17 | 18 | private final Instrumentation inst; 19 | private final Sleeper sleeper; 20 | 21 | /** 22 | * Constructs this object. 23 | * 24 | * @param inst the {@code Instrumentation} instance 25 | * @param sleeper the {@code Sleeper} instance 26 | */ 27 | 28 | Sender(Instrumentation inst, Sleeper sleeper) { 29 | this.inst = inst; 30 | this.sleeper = sleeper; 31 | } 32 | 33 | /** 34 | * Tells Robotium to send a key code: Right, Left, Up, Down, Enter or other. 35 | * 36 | * @param keycode the key code to be sent. Use {@link KeyEvent#KEYCODE_ENTER}, {@link KeyEvent#KEYCODE_MENU}, {@link KeyEvent#KEYCODE_DEL}, {@link KeyEvent#KEYCODE_DPAD_RIGHT} and so on 37 | */ 38 | 39 | public void sendKeyCode(int keycode) 40 | { 41 | sleeper.sleep(); 42 | try{ 43 | inst.sendCharacterSync(keycode); 44 | }catch(SecurityException e){ 45 | Assert.fail("Can not complete action! ("+(e != null ? e.getClass().getName()+": "+e.getMessage() : "null")+")"); 46 | } 47 | } 48 | 49 | /** 50 | * Simulates pressing the hardware back key. 51 | */ 52 | 53 | public void goBack() { 54 | sleeper.sleep(); 55 | try { 56 | inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 57 | sleeper.sleep(); 58 | } catch (Throwable ignored) {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Setter.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | import android.widget.DatePicker; 6 | import android.widget.ProgressBar; 7 | import android.widget.SlidingDrawer; 8 | import android.widget.TimePicker; 9 | 10 | 11 | /** 12 | * Contains set methods. Examples are setDatePicker(), 13 | * setTimePicker(). 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | class Setter{ 20 | 21 | private final int CLOSED = 0; 22 | private final int OPENED = 1; 23 | private final ActivityUtils activityUtils; 24 | private final Getter getter; 25 | private final Clicker clicker; 26 | private final Waiter waiter; 27 | 28 | /** 29 | * Constructs this object. 30 | * 31 | * @param activityUtils the {@code ActivityUtils} instance 32 | * @param getter the {@code Getter} instance 33 | * @param clicker the {@code Clicker} instance 34 | * @param waiter the {@code Waiter} instance 35 | */ 36 | 37 | public Setter(ActivityUtils activityUtils, Getter getter, Clicker clicker, Waiter waiter) { 38 | this.activityUtils = activityUtils; 39 | this.getter = getter; 40 | this.clicker = clicker; 41 | this.waiter = waiter; 42 | } 43 | 44 | 45 | /** 46 | * Sets the date in a given {@link DatePicker}. 47 | * 48 | * @param datePicker the {@code DatePicker} object. 49 | * @param year the year e.g. 2011 50 | * @param monthOfYear the month which is starting from zero e.g. 03 51 | * @param dayOfMonth the day e.g. 10 52 | */ 53 | 54 | public void setDatePicker(final DatePicker datePicker, final int year, final int monthOfYear, final int dayOfMonth) { 55 | if(datePicker != null){ 56 | Activity activity = activityUtils.getCurrentActivity(false); 57 | if(activity != null){ 58 | activity.runOnUiThread(new Runnable() 59 | { 60 | public void run() 61 | { 62 | try{ 63 | datePicker.updateDate(year, monthOfYear, dayOfMonth); 64 | }catch (Exception ignored){} 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | 71 | 72 | /** 73 | * Sets the time in a given {@link TimePicker}. 74 | * 75 | * @param timePicker the {@code TimePicker} object. 76 | * @param hour the hour e.g. 15 77 | * @param minute the minute e.g. 30 78 | */ 79 | 80 | public void setTimePicker(final TimePicker timePicker, final int hour, final int minute) { 81 | if(timePicker != null){ 82 | Activity activity = activityUtils.getCurrentActivity(false); 83 | if(activity != null){ 84 | activity.runOnUiThread(new Runnable() 85 | { 86 | public void run() 87 | { 88 | try{ 89 | timePicker.setCurrentHour(hour); 90 | timePicker.setCurrentMinute(minute); 91 | }catch (Exception ignored){} 92 | } 93 | }); 94 | } 95 | } 96 | } 97 | 98 | 99 | /** 100 | * Sets the progress of a given {@link ProgressBar}. Examples are SeekBar and RatingBar. 101 | * @param progressBar the {@code ProgressBar} 102 | * @param progress the progress that the {@code ProgressBar} should be set to 103 | */ 104 | 105 | public void setProgressBar(final ProgressBar progressBar,final int progress) { 106 | if(progressBar != null){ 107 | Activity activity = activityUtils.getCurrentActivity(false); 108 | if(activity != null){ 109 | activity.runOnUiThread(new Runnable() 110 | { 111 | public void run() 112 | { 113 | try{ 114 | progressBar.setProgress(progress); 115 | }catch (Exception ignored){} 116 | } 117 | }); 118 | } 119 | } 120 | } 121 | 122 | 123 | /** 124 | * Sets the status of a given SlidingDrawer. Examples are Solo.CLOSED and Solo.OPENED. 125 | * 126 | * @param slidingDrawer the {@link SlidingDrawer} 127 | * @param status the status that the {@link SlidingDrawer} should be set to 128 | */ 129 | 130 | public void setSlidingDrawer(final SlidingDrawer slidingDrawer, final int status){ 131 | if(slidingDrawer != null){ 132 | Activity activity = activityUtils.getCurrentActivity(false); 133 | if(activity != null){ 134 | activity.runOnUiThread(new Runnable() 135 | { 136 | public void run() 137 | { 138 | try{ 139 | switch (status) { 140 | case CLOSED: 141 | slidingDrawer.close(); 142 | break; 143 | case OPENED: 144 | slidingDrawer.open(); 145 | break; 146 | } 147 | }catch (Exception ignored){} 148 | } 149 | }); 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Sets the status of the NavigationDrawer. Examples are Solo.CLOSED and Solo.OPENED. 156 | * 157 | * @param status the status that the {@link NavigationDrawer} should be set to 158 | */ 159 | 160 | public void setNavigationDrawer(final int status){ 161 | final View homeView = getter.getView("home", 0); 162 | final View leftDrawer = getter.getView("left_drawer", 0); 163 | 164 | try{ 165 | switch (status) { 166 | 167 | case CLOSED: 168 | if(leftDrawer != null && homeView != null && leftDrawer.isShown()){ 169 | clicker.clickOnScreen(homeView); 170 | } 171 | break; 172 | 173 | case OPENED: 174 | if(leftDrawer != null && homeView != null && !leftDrawer.isShown()){ 175 | clicker.clickOnScreen(homeView); 176 | 177 | Condition condition = new Condition() { 178 | 179 | @Override 180 | public boolean isSatisfied() { 181 | return leftDrawer.isShown(); 182 | } 183 | }; 184 | waiter.waitForCondition(condition, Timeout.getSmallTimeout()); 185 | } 186 | break; 187 | } 188 | }catch (Exception ignored){} 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Sleeper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | class Sleeper { 4 | 5 | private int pauseDuration; 6 | private int miniPauseDuration; 7 | 8 | private Sleeper() { 9 | 10 | } 11 | 12 | /** 13 | * Constructs this object. 14 | * 15 | * @param pauseDuration pause duration used in {@code sleep} 16 | * @param miniPauseDuration pause duration used in {@code sleepMini} 17 | */ 18 | 19 | public Sleeper(int pauseDuration, int miniPauseDuration) { 20 | this.pauseDuration = pauseDuration; 21 | this.miniPauseDuration = miniPauseDuration; 22 | } 23 | 24 | /** 25 | * Sleeps the current thread for the pause length. 26 | */ 27 | 28 | public void sleep() { 29 | sleep(pauseDuration); 30 | } 31 | 32 | 33 | /** 34 | * Sleeps the current thread for the mini pause length. 35 | */ 36 | 37 | public void sleepMini() { 38 | sleep(miniPauseDuration); 39 | } 40 | 41 | 42 | /** 43 | * Sleeps the current thread for time milliseconds. 44 | * 45 | * @param time the length of the sleep in milliseconds 46 | */ 47 | 48 | public void sleep(int time) { 49 | try { 50 | Thread.sleep(time); 51 | } catch (InterruptedException ignored) {} 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Swiper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.MotionEvent; 7 | import android.view.MotionEvent.PointerCoords; 8 | import android.view.MotionEvent.PointerProperties; 9 | 10 | class Swiper 11 | { 12 | private final Instrumentation _instrument; 13 | public static final int GESTURE_DURATION_MS = 1000; 14 | public static final int EVENT_TIME_INTERVAL_MS = 10; 15 | 16 | public Swiper(Instrumentation inst) 17 | { 18 | this._instrument = inst; 19 | } 20 | 21 | public void generateSwipeGesture(PointF startPoint1, PointF startPoint2, 22 | PointF endPoint1, PointF endPoint2) 23 | { 24 | 25 | long downTime = SystemClock.uptimeMillis(); 26 | long eventTime = SystemClock.uptimeMillis(); 27 | 28 | float startX1 = startPoint1.x; 29 | float startY1 = startPoint1.y; 30 | float startX2 = startPoint2.x; 31 | float startY2 = startPoint2.y; 32 | 33 | float endX1 = endPoint1.x; 34 | float endY1 = endPoint1.y; 35 | float endX2 = endPoint2.x; 36 | float endY2 = endPoint2.y; 37 | 38 | // pointer 1 39 | float x1 = startX1; 40 | float y1 = startY1; 41 | 42 | // pointer 2 43 | float x2 = startX2; 44 | float y2 = startY2; 45 | 46 | PointerCoords[] pointerCoords = new PointerCoords[2]; 47 | PointerCoords pc1 = new PointerCoords(); 48 | PointerCoords pc2 = new PointerCoords(); 49 | pc1.x = x1; 50 | pc1.y = y1; 51 | pc1.pressure = 1; 52 | pc1.size = 1; 53 | pc2.x = x2; 54 | pc2.y = y2; 55 | pc2.pressure = 1; 56 | pc2.size = 1; 57 | pointerCoords[0] = pc1; 58 | pointerCoords[1] = pc2; 59 | 60 | PointerProperties[] pointerProperties = new PointerProperties[2]; 61 | PointerProperties pp1 = new PointerProperties(); 62 | PointerProperties pp2 = new PointerProperties(); 63 | pp1.id = 0; 64 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 65 | pp2.id = 1; 66 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 67 | pointerProperties[0] = pp1; 68 | pointerProperties[1] = pp2; 69 | 70 | MotionEvent event; 71 | // send the initial touches 72 | event = MotionEvent.obtain(downTime, eventTime, 73 | MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords, 74 | 0, 0, // metaState, buttonState 75 | 1, // x precision 76 | 1, // y precision 77 | 0, 0, 0, 0); // deviceId, edgeFlags, source, flags 78 | _instrument.sendPointerSync(event); 79 | 80 | event = MotionEvent.obtain(downTime, eventTime, 81 | MotionEvent.ACTION_POINTER_DOWN 82 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 83 | 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0); 84 | _instrument.sendPointerSync(event); 85 | 86 | int numMoves = GESTURE_DURATION_MS / EVENT_TIME_INTERVAL_MS; 87 | 88 | float stepX1 = (endX1 - startX1) / numMoves; 89 | float stepY1 = (endY1 - startY1) / numMoves; 90 | float stepX2 = (endX2 - startX2) / numMoves; 91 | float stepY2 = (endY2 - startY2) / numMoves; 92 | 93 | // send the zoom 94 | for (int i = 0; i < numMoves; i++) 95 | { 96 | eventTime += EVENT_TIME_INTERVAL_MS; 97 | pointerCoords[0].x += stepX1; 98 | pointerCoords[0].y += stepY1; 99 | pointerCoords[1].x += stepX2; 100 | pointerCoords[1].y += stepY2; 101 | 102 | event = MotionEvent.obtain(downTime, eventTime, 103 | MotionEvent.ACTION_MOVE, 2, pointerProperties, 104 | pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0); 105 | _instrument.sendPointerSync(event); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/SystemUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | 4 | import java.lang.reflect.Method; 5 | import android.app.Instrumentation; 6 | import android.content.Context; 7 | import android.net.ConnectivityManager; 8 | import android.net.wifi.WifiManager; 9 | 10 | 11 | /** 12 | * Contains System methods. Examples are: setDeviceLocale(String language, String country), 13 | * setMobileData(Boolean turnedOn). 14 | * 15 | * @author Renas Reda, renas.reda@robotium.com 16 | * 17 | */ 18 | 19 | public class SystemUtils { 20 | private Instrumentation instrumentation; 21 | 22 | public SystemUtils(Instrumentation instrumentation){ 23 | this.instrumentation = instrumentation; 24 | } 25 | 26 | 27 | /** 28 | * Sets if mobile data should be turned on or off. Requires android.permission.CHANGE_NETWORK_STATE in the AndroidManifest.xml of the application under test. 29 | * 30 | * @param turnedOn true if mobile data is to be turned on and false if not 31 | */ 32 | 33 | public void setMobileData(Boolean turnedOn){ 34 | ConnectivityManager dataManager=(ConnectivityManager)instrumentation.getTargetContext().getSystemService(Context.CONNECTIVITY_SERVICE); 35 | 36 | Method dataClass = null; 37 | try { 38 | dataClass = ConnectivityManager.class.getDeclaredMethod("setMobileDataEnabled", boolean.class); 39 | dataClass.setAccessible(true); 40 | dataClass.invoke(dataManager, turnedOn); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | /** 47 | * Sets if wifi data should be turned on or off. Requires android.permission.CHANGE_WIFI_STATE in the AndroidManifest.xml of the application under test. 48 | * 49 | * 50 | * @param turnedOn true if mobile wifi is to be turned on and false if not 51 | */ 52 | 53 | public void setWiFiData(Boolean turnedOn){ 54 | WifiManager wifiManager = (WifiManager)instrumentation.getTargetContext().getSystemService(Context.WIFI_SERVICE); 55 | try{ 56 | wifiManager.setWifiEnabled(turnedOn); 57 | }catch(Exception e){ 58 | e.printStackTrace(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Tapper.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.graphics.PointF; 5 | import android.os.SystemClock; 6 | import android.view.InputDevice; 7 | import android.view.MotionEvent; 8 | import android.view.MotionEvent.PointerCoords; 9 | import android.view.MotionEvent.PointerProperties; 10 | 11 | class Tapper 12 | { 13 | private final Instrumentation _instrument; 14 | public static final int GESTURE_DURATION_MS = 1000; 15 | public static final int EVENT_TIME_INTERVAL_MS = 10; 16 | 17 | public Tapper(Instrumentation inst) 18 | { 19 | this._instrument = inst; 20 | } 21 | 22 | public void generateTapGesture(int numTaps, PointF... points) 23 | { 24 | MotionEvent event; 25 | 26 | long downTime = SystemClock.uptimeMillis(); 27 | long eventTime = SystemClock.uptimeMillis(); 28 | 29 | // pointer 1 30 | float x1 = points[0].x; 31 | float y1 = points[0].y; 32 | 33 | float x2 = 0; 34 | float y2 = 0; 35 | if (points.length == 2) 36 | { 37 | // pointer 2 38 | x2 = points[1].x; 39 | y2 = points[1].y; 40 | } 41 | 42 | PointerCoords[] pointerCoords = new PointerCoords[points.length]; 43 | PointerCoords pc1 = new PointerCoords(); 44 | pc1.x = x1; 45 | pc1.y = y1; 46 | pc1.pressure = 1; 47 | pc1.size = 1; 48 | pointerCoords[0] = pc1; 49 | PointerCoords pc2 = new PointerCoords(); 50 | if (points.length == 2) 51 | { 52 | pc2.x = x2; 53 | pc2.y = y2; 54 | pc2.pressure = 1; 55 | pc2.size = 1; 56 | pointerCoords[1] = pc2; 57 | } 58 | 59 | PointerProperties[] pointerProperties = new PointerProperties[points.length]; 60 | PointerProperties pp1 = new PointerProperties(); 61 | pp1.id = 0; 62 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 63 | pointerProperties[0] = pp1; 64 | PointerProperties pp2 = new PointerProperties(); 65 | if (points.length == 2) 66 | { 67 | pp2.id = 1; 68 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 69 | pointerProperties[1] = pp2; 70 | } 71 | 72 | int i = 0; 73 | while (i != numTaps) 74 | { 75 | event = MotionEvent.obtain(downTime, eventTime, 76 | MotionEvent.ACTION_DOWN, points.length, pointerProperties, 77 | pointerCoords, 0, 0, 1, 1, 0, 0, 78 | InputDevice.SOURCE_TOUCHSCREEN, 0); 79 | _instrument.sendPointerSync(event); 80 | 81 | if (points.length == 2) 82 | { 83 | event = MotionEvent 84 | .obtain(downTime, 85 | eventTime, 86 | MotionEvent.ACTION_POINTER_DOWN 87 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 88 | points.length, pointerProperties, 89 | pointerCoords, 0, 0, 1, 1, 0, 0, 90 | InputDevice.SOURCE_TOUCHSCREEN, 0); 91 | _instrument.sendPointerSync(event); 92 | 93 | eventTime += EVENT_TIME_INTERVAL_MS; 94 | event = MotionEvent 95 | .obtain(downTime, 96 | eventTime, 97 | MotionEvent.ACTION_POINTER_UP 98 | + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 99 | points.length, pointerProperties, 100 | pointerCoords, 0, 0, 1, 1, 0, 0, 101 | InputDevice.SOURCE_TOUCHSCREEN, 0); 102 | _instrument.sendPointerSync(event); 103 | } 104 | 105 | eventTime += EVENT_TIME_INTERVAL_MS; 106 | event = MotionEvent.obtain(downTime, eventTime, 107 | MotionEvent.ACTION_UP, points.length, pointerProperties, 108 | pointerCoords, 0, 0, 1, 1, 0, 0, 109 | InputDevice.SOURCE_TOUCHSCREEN, 0); 110 | _instrument.sendPointerSync(event); 111 | 112 | i++; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/TextEnterer.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import junit.framework.Assert; 4 | import android.app.Instrumentation; 5 | import android.text.InputType; 6 | import android.widget.EditText; 7 | 8 | 9 | /** 10 | * Contains setEditText() to enter text into text fields. 11 | * 12 | * @author Renas Reda, renas.reda@robotium.com 13 | * 14 | */ 15 | 16 | class TextEnterer{ 17 | 18 | private final Instrumentation inst; 19 | private final Clicker clicker; 20 | private final DialogUtils dialogUtils; 21 | 22 | /** 23 | * Constructs this object. 24 | * 25 | * @param inst the {@code Instrumentation} instance 26 | * @param clicker the {@code Clicker} instance 27 | * @param dialogUtils the {@code DialogUtils} instance 28 | * 29 | */ 30 | 31 | public TextEnterer(Instrumentation inst, Clicker clicker, DialogUtils dialogUtils) { 32 | this.inst = inst; 33 | this.clicker = clicker; 34 | this.dialogUtils = dialogUtils; 35 | } 36 | 37 | /** 38 | * Sets an {@code EditText} text 39 | * 40 | * @param index the index of the {@code EditText} 41 | * @param text the text that should be set 42 | */ 43 | 44 | public void setEditText(final EditText editText, final String text) { 45 | if(editText != null){ 46 | final String previousText = editText.getText().toString(); 47 | 48 | inst.runOnMainSync(new Runnable() 49 | { 50 | public void run() 51 | { 52 | editText.setInputType(InputType.TYPE_NULL); 53 | editText.performClick(); 54 | dialogUtils.hideSoftKeyboard(editText, false, false); 55 | if(text.equals("")) 56 | editText.setText(text); 57 | else{ 58 | editText.setText(previousText + text); 59 | editText.setCursorVisible(false); 60 | } 61 | } 62 | }); 63 | } 64 | } 65 | 66 | /** 67 | * Types text in an {@code EditText} 68 | * 69 | * @param index the index of the {@code EditText} 70 | * @param text the text that should be typed 71 | */ 72 | 73 | public void typeText(final EditText editText, final String text){ 74 | if(editText != null){ 75 | inst.runOnMainSync(new Runnable() 76 | { 77 | public void run() 78 | { 79 | editText.setInputType(InputType.TYPE_NULL); 80 | } 81 | }); 82 | clicker.clickOnScreen(editText, false, 0); 83 | dialogUtils.hideSoftKeyboard(editText, true, true); 84 | 85 | boolean successfull = false; 86 | int retry = 0; 87 | 88 | while(!successfull && retry < 10) { 89 | 90 | try{ 91 | inst.sendStringSync(text); 92 | successfull = true; 93 | }catch(SecurityException e){ 94 | dialogUtils.hideSoftKeyboard(editText, true, true); 95 | retry++; 96 | } 97 | } 98 | if(!successfull) { 99 | Assert.fail("Text can not be typed!"); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Timeout.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import com.robotium.solo.Solo.Config; 4 | 5 | 6 | 7 | 8 | /** 9 | * Used to get and set the default timeout lengths of the various Solo methods. 10 | * 11 | * @author Renas Reda, renas.reda@robotium.com 12 | * 13 | */ 14 | 15 | public class Timeout{ 16 | 17 | private static int largeTimeout; 18 | private static int smallTimeout; 19 | 20 | 21 | /** 22 | * Sets the default timeout length of the waitFor methods. Will fall back to the default values set by {@link Config}. 23 | *

24 | * Timeout can also be set through adb shell (requires root access): 25 | *

26 | * 'adb shell setprop solo_large_timeout milliseconds' 27 | * 28 | * @param milliseconds the default timeout length of the waitFor methods 29 | * 30 | */ 31 | public static void setLargeTimeout(int milliseconds){ 32 | largeTimeout = milliseconds; 33 | } 34 | 35 | /** 36 | * Sets the default timeout length of the get, is, set, assert, enter, type and click methods. Will fall back to the default values set by {@link Config}. 37 | *

38 | * Timeout can also be set through adb shell (requires root access): 39 | *

40 | * 'adb shell setprop solo_small_timeout milliseconds' 41 | * 42 | * @param milliseconds the default timeout length of the get, is, set, assert, enter and click methods 43 | * 44 | */ 45 | public static void setSmallTimeout(int milliseconds){ 46 | smallTimeout = milliseconds; 47 | } 48 | 49 | /** 50 | * Gets the default timeout length of the waitFor methods. 51 | * 52 | * @return the timeout length in milliseconds 53 | * 54 | */ 55 | public static int getLargeTimeout(){ 56 | return largeTimeout; 57 | } 58 | 59 | /** 60 | * Gets the default timeout length of the get, is, set, assert, enter, type and click methods. 61 | * 62 | * @return the timeout length in milliseconds 63 | * 64 | */ 65 | public static int getSmallTimeout(){ 66 | return smallTimeout; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/ViewLocationComparator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.view.View; 4 | import java.util.Comparator; 5 | 6 | /** 7 | * Orders {@link View}s by their location on-screen. 8 | * 9 | */ 10 | 11 | class ViewLocationComparator implements Comparator { 12 | 13 | private final int a[] = new int[2]; 14 | private final int b[] = new int[2]; 15 | private final int axis1, axis2; 16 | 17 | public ViewLocationComparator() { 18 | this(true); 19 | } 20 | 21 | /** 22 | * @param yAxisFirst Whether the y-axis should be compared before the x-axis. 23 | */ 24 | 25 | public ViewLocationComparator(boolean yAxisFirst) { 26 | this.axis1 = yAxisFirst ? 1 : 0; 27 | this.axis2 = yAxisFirst ? 0 : 1; 28 | } 29 | 30 | public int compare(View lhs, View rhs) { 31 | lhs.getLocationOnScreen(a); 32 | rhs.getLocationOnScreen(b); 33 | 34 | if (a[axis1] != b[axis1]) { 35 | return a[axis1] < b[axis1] ? -1 : 1; 36 | } 37 | if (a[axis2] < b[axis2]) { 38 | return -1; 39 | } 40 | return a[axis2] == b[axis2] ? 0 : 1; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/WebElement.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.Hashtable; 4 | 5 | /** 6 | * Represents an element shown in a WebView. 7 | * 8 | * @author Renas Reda, renas.reda@robotium.com 9 | * 10 | */ 11 | 12 | public class WebElement { 13 | 14 | private int locationX = 0; 15 | private int locationY = 0; 16 | private String id; 17 | private String text; 18 | private String name; 19 | private String className; 20 | private String tagName; 21 | private Hashtable attributes; 22 | 23 | 24 | /** 25 | * Constructs this object. 26 | * 27 | * @param webId the given web id 28 | * @param textContent the given text to be set 29 | * @param name the given name to be set 30 | * @param className the given class name to set 31 | * @param tagName the given tag name to be set 32 | * @param attributes the attributes to set 33 | */ 34 | 35 | public WebElement(String webId, String textContent, String name, String className, String tagName, Hashtable attributes) { 36 | 37 | this.setId(webId); 38 | this.setTextContent(textContent); 39 | this.setName(name); 40 | this.setClassName(className); 41 | this.setTagName(tagName); 42 | this.setAttributes(attributes); 43 | } 44 | 45 | /** 46 | * Returns the WebElements location on screen. 47 | */ 48 | 49 | public void getLocationOnScreen(int[] location) { 50 | 51 | location[0] = locationX; 52 | location[1] = locationY; 53 | } 54 | 55 | /** 56 | * Sets the X location. 57 | * 58 | * @param locationX the X location of the {@code WebElement} 59 | */ 60 | 61 | public void setLocationX(int locationX){ 62 | this.locationX = locationX; 63 | } 64 | 65 | /** 66 | * Sets the Y location. 67 | * 68 | * @param locationY the Y location of the {@code WebElement} 69 | */ 70 | 71 | public void setLocationY(int locationY){ 72 | this.locationY = locationY; 73 | } 74 | 75 | /** 76 | * Returns the X location. 77 | * 78 | * @return the X location 79 | */ 80 | 81 | public int getLocationX(){ 82 | return this.locationX; 83 | } 84 | 85 | /** 86 | * Returns the Y location. 87 | * 88 | * @return the Y location 89 | */ 90 | 91 | public int getLocationY(){ 92 | return this.locationY; 93 | } 94 | 95 | /** 96 | * Returns the id. 97 | * 98 | * @return the id 99 | */ 100 | 101 | public String getId() { 102 | return id; 103 | } 104 | 105 | /** 106 | * Sets the id. 107 | * 108 | * @param id the id to set 109 | */ 110 | 111 | public void setId(String id) { 112 | this.id = id; 113 | } 114 | 115 | /** 116 | * Returns the name. 117 | * 118 | * @return the name 119 | */ 120 | 121 | public String getName() { 122 | return name; 123 | } 124 | 125 | /** 126 | * Sets the name. 127 | * 128 | * @param name the name to set 129 | */ 130 | 131 | public void setName(String name) { 132 | this.name = name; 133 | } 134 | 135 | /** 136 | * Returns the class name. 137 | * 138 | * @return the class name 139 | */ 140 | 141 | public String getClassName() { 142 | return className; 143 | } 144 | 145 | /** 146 | * Sets the class name. 147 | * 148 | * @param className the class name to set 149 | */ 150 | 151 | public void setClassName(String className) { 152 | this.className = className; 153 | } 154 | 155 | /** 156 | * Returns the tag name. 157 | * 158 | * @return the tag name 159 | */ 160 | 161 | public String getTagName() { 162 | return tagName; 163 | } 164 | 165 | /** 166 | * Sets the tag name. 167 | * 168 | * @param tagName the tag name to set 169 | */ 170 | 171 | public void setTagName(String tagName) { 172 | this.tagName = tagName; 173 | } 174 | 175 | /** 176 | * Returns the text content. 177 | * 178 | * @return the text content 179 | */ 180 | 181 | public String getText() { 182 | return text; 183 | } 184 | 185 | /** 186 | * Sets the text content. 187 | * 188 | * @param textContent the text content to set 189 | */ 190 | 191 | public void setTextContent(String textContent) { 192 | this.text = textContent; 193 | } 194 | 195 | /** 196 | * Returns the value for the specified attribute. 197 | * 198 | * @return the value for the specified attribute 199 | */ 200 | 201 | public String getAttribute(String attributeName) { 202 | if (attributeName != null){ 203 | return this.attributes.get(attributeName); 204 | } 205 | 206 | return null; 207 | } 208 | 209 | /** 210 | * Sets the attributes. 211 | * 212 | * @param attributes the attributes to set 213 | */ 214 | 215 | public void setAttributes(Hashtable attributes) { 216 | this.attributes = attributes; 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/WebElementCreator.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Hashtable; 5 | import java.util.List; 6 | import java.util.concurrent.CopyOnWriteArrayList; 7 | import android.os.SystemClock; 8 | import android.webkit.WebView; 9 | 10 | /** 11 | * Contains TextView related methods. Examples are: 12 | * getTextViewsFromWebViews(), createTextViewAndAddInList(). 13 | * 14 | * @author Renas Reda, renas.reda@robotium.com 15 | * 16 | */ 17 | 18 | class WebElementCreator { 19 | 20 | private List webElements; 21 | private Sleeper sleeper; 22 | private boolean isFinished = false; 23 | 24 | /** 25 | * Constructs this object. 26 | * 27 | * @param sleeper the {@code Sleeper} instance 28 | * 29 | */ 30 | 31 | public WebElementCreator(Sleeper sleeper){ 32 | this.sleeper = sleeper; 33 | webElements = new CopyOnWriteArrayList(); 34 | } 35 | 36 | /** 37 | * Prepares for start of creating {@code TextView} objects based on web elements 38 | */ 39 | 40 | public void prepareForStart(){ 41 | setFinished(false); 42 | webElements.clear(); 43 | } 44 | 45 | /** 46 | * Returns an {@code ArrayList} of {@code TextView} objects based on the web elements shown 47 | * 48 | * @return an {@code ArrayList} of {@code TextView} objects based on the web elements shown 49 | */ 50 | 51 | public ArrayList getWebElementsFromWebViews(){ 52 | waitForWebElementsToBeCreated(); 53 | return new ArrayList(webElements); 54 | } 55 | 56 | /** 57 | * Returns true if all {@code TextView} objects based on web elements have been created 58 | * 59 | * @return true if all {@code TextView} objects based on web elements have been created 60 | */ 61 | 62 | public boolean isFinished(){ 63 | return isFinished; 64 | } 65 | 66 | 67 | /** 68 | * Set to true if all {@code TextView} objects have been created 69 | * 70 | * @param isFinished true if all {@code TextView} objects have been created 71 | */ 72 | 73 | public void setFinished(boolean isFinished){ 74 | this.isFinished = isFinished; 75 | } 76 | 77 | /** 78 | * Creates a {@ WebElement} object from the given text and {@code WebView} 79 | * 80 | * @param webData the data of the web element 81 | * @param webView the {@code WebView} the text is shown in 82 | */ 83 | 84 | public void createWebElementAndAddInList(String webData, WebView webView){ 85 | 86 | WebElement webElement = createWebElementAndSetLocation(webData, webView); 87 | 88 | if((webElement!=null)) 89 | webElements.add(webElement); 90 | } 91 | 92 | /** 93 | * Sets the location of a {@code WebElement} 94 | * 95 | * @param webElement the {@code TextView} object to set location 96 | * @param webView the {@code WebView} the text is shown in 97 | * @param x the x location to set 98 | * @param y the y location to set 99 | * @param width the width to set 100 | * @param height the height to set 101 | */ 102 | 103 | private void setLocation(WebElement webElement, WebView webView, int x, int y, int width, int height ){ 104 | float scale = webView.getScale(); 105 | int[] locationOfWebViewXY = new int[2]; 106 | webView.getLocationOnScreen(locationOfWebViewXY); 107 | 108 | int locationX = (int) (locationOfWebViewXY[0] + (x + (Math.floor(width / 2))) * scale); 109 | int locationY = (int) (locationOfWebViewXY[1] + (y + (Math.floor(height / 2))) * scale); 110 | 111 | webElement.setLocationX(locationX); 112 | webElement.setLocationY(locationY); 113 | } 114 | 115 | /** 116 | * Creates a {@code WebView} object 117 | * 118 | * @param information the data of the web element 119 | * @param webView the web view the text is shown in 120 | * 121 | * @return a {@code WebElement} object with a given text and location 122 | */ 123 | 124 | private WebElement createWebElementAndSetLocation(String information, WebView webView){ 125 | String[] data = information.split(";,"); 126 | String[] elements = null; 127 | int x = 0; 128 | int y = 0; 129 | int width = 0; 130 | int height = 0; 131 | Hashtable attributes = new Hashtable(); 132 | try{ 133 | x = Math.round(Float.valueOf(data[5])); 134 | y = Math.round(Float.valueOf(data[6])); 135 | width = Math.round(Float.valueOf(data[7])); 136 | height = Math.round(Float.valueOf(data[8])); 137 | elements = data[9].split("\\#\\$"); 138 | }catch(Exception ignored){} 139 | 140 | if(elements != null) { 141 | for (int index = 0; index < elements.length; index++){ 142 | String[] element = elements[index].split("::"); 143 | if (element.length > 1) { 144 | attributes.put(element[0], element[1]); 145 | } else { 146 | attributes.put(element[0], element[0]); 147 | } 148 | } 149 | } 150 | 151 | WebElement webElement = null; 152 | 153 | try{ 154 | webElement = new WebElement(data[0], data[1], data[2], data[3], data[4], attributes); 155 | setLocation(webElement, webView, x, y, width, height); 156 | }catch(Exception ignored) {} 157 | 158 | return webElement; 159 | } 160 | 161 | /** 162 | * Waits for {@code WebElement} objects to be created 163 | * 164 | * @return true if successfully created before timout 165 | */ 166 | 167 | private boolean waitForWebElementsToBeCreated(){ 168 | final long endTime = SystemClock.uptimeMillis() + 5000; 169 | 170 | while(SystemClock.uptimeMillis() < endTime){ 171 | 172 | if(isFinished){ 173 | return true; 174 | } 175 | 176 | sleeper.sleepMini(); 177 | } 178 | return false; 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/WebUtils.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.regex.Pattern; 10 | import com.robotium.solo.Solo.Config; 11 | import android.app.Instrumentation; 12 | import android.webkit.WebChromeClient; 13 | import android.webkit.WebView; 14 | import android.widget.TextView; 15 | 16 | 17 | /** 18 | * Contains web related methods. Examples are: 19 | * enterTextIntoWebElement(), getWebTexts(), getWebElements(). 20 | * 21 | * @author Renas Reda, renas.reda@robotium.com 22 | * 23 | */ 24 | 25 | class WebUtils { 26 | 27 | private ViewFetcher viewFetcher; 28 | private Instrumentation inst; 29 | RobotiumWebClient robotiumWebCLient; 30 | WebElementCreator webElementCreator; 31 | WebChromeClient originalWebChromeClient = null; 32 | private Config config; 33 | 34 | 35 | /** 36 | * Constructs this object. 37 | * 38 | * @param config the {@code Config} instance 39 | * @param instrumentation the {@code Instrumentation} instance 40 | * @param viewFetcher the {@code ViewFetcher} 41 | * @param sleeper the {@code Sleeper} instance 42 | */ 43 | 44 | public WebUtils(Config config, Instrumentation instrumentation, ViewFetcher viewFetcher, Sleeper sleeper){ 45 | this.config = config; 46 | this.inst = instrumentation; 47 | this.viewFetcher = viewFetcher; 48 | webElementCreator = new WebElementCreator(sleeper); 49 | robotiumWebCLient = new RobotiumWebClient(instrumentation, webElementCreator); 50 | } 51 | 52 | /** 53 | * Returns {@code TextView} objects based on web elements shown in the present WebViews 54 | * 55 | * @param onlyFromVisibleWebViews true if only from visible WebViews 56 | * @return an {@code ArrayList} of {@code TextViews}s created from the present {@code WebView}s 57 | */ 58 | 59 | public ArrayList getTextViewsFromWebView(){ 60 | boolean javaScriptWasExecuted = executeJavaScriptFunction("allTexts();"); 61 | 62 | return createAndReturnTextViewsFromWebElements(javaScriptWasExecuted); 63 | } 64 | 65 | /** 66 | * Creates and returns TextView objects based on WebElements 67 | * 68 | * @return an ArrayList with TextViews 69 | */ 70 | 71 | private ArrayList createAndReturnTextViewsFromWebElements(boolean javaScriptWasExecuted){ 72 | ArrayList webElementsAsTextViews = new ArrayList(); 73 | 74 | if(javaScriptWasExecuted){ 75 | for(WebElement webElement : webElementCreator.getWebElementsFromWebViews()){ 76 | if(isWebElementSufficientlyShown(webElement)){ 77 | RobotiumTextView textView = new RobotiumTextView(inst.getContext(), webElement.getText(), webElement.getLocationX(), webElement.getLocationY()); 78 | webElementsAsTextViews.add(textView); 79 | } 80 | } 81 | } 82 | return webElementsAsTextViews; 83 | } 84 | 85 | /** 86 | * Returns an ArrayList of WebElements currently shown in the active WebView. 87 | * 88 | * @param onlySufficientlyVisible true if only sufficiently visible {@link WebElement} objects should be returned 89 | * @return an {@code ArrayList} of the {@link WebElement} objects shown in the active WebView 90 | */ 91 | 92 | public ArrayList getWebElements(boolean onlySufficientlyVisible){ 93 | boolean javaScriptWasExecuted = executeJavaScriptFunction("allWebElements();"); 94 | 95 | return getWebElements(javaScriptWasExecuted, onlySufficientlyVisible); 96 | } 97 | 98 | /** 99 | * Returns an ArrayList of WebElements of the specified By object currently shown in the active WebView. 100 | * 101 | * @param by the By object. Examples are By.id("id") and By.name("name") 102 | * @param onlySufficientlyVisible true if only sufficiently visible {@link WebElement} objects should be returned 103 | * @return an {@code ArrayList} of the {@link WebElement} objects currently shown in the active WebView 104 | */ 105 | 106 | public ArrayList getWebElements(final By by, boolean onlySufficientlyVisbile){ 107 | boolean javaScriptWasExecuted = executeJavaScript(by, false); 108 | 109 | if(config.useJavaScriptToClickWebElements){ 110 | if(!javaScriptWasExecuted){ 111 | return new ArrayList(); 112 | } 113 | return webElementCreator.getWebElementsFromWebViews(); 114 | } 115 | 116 | return getWebElements(javaScriptWasExecuted, onlySufficientlyVisbile); 117 | } 118 | 119 | /** 120 | * Returns the sufficiently shown WebElements 121 | * 122 | * @param javaScriptWasExecuted true if JavaScript was executed 123 | * @param onlySufficientlyVisible true if only sufficiently visible {@link WebElement} objects should be returned 124 | * @return the sufficiently shown WebElements 125 | */ 126 | 127 | private ArrayList getWebElements(boolean javaScriptWasExecuted, boolean onlySufficientlyVisbile){ 128 | ArrayList webElements = new ArrayList(); 129 | 130 | if(javaScriptWasExecuted){ 131 | for(WebElement webElement : webElementCreator.getWebElementsFromWebViews()){ 132 | if(!onlySufficientlyVisbile){ 133 | webElements.add(webElement); 134 | } 135 | else if(isWebElementSufficientlyShown(webElement)){ 136 | webElements.add(webElement); 137 | } 138 | } 139 | } 140 | return webElements; 141 | } 142 | 143 | /** 144 | * Prepares for start of JavaScript execution 145 | * 146 | * @return the JavaScript as a String 147 | */ 148 | 149 | private String prepareForStartOfJavascriptExecution(List webViews) { 150 | webElementCreator.prepareForStart(); 151 | 152 | WebChromeClient currentWebChromeClient = getCurrentWebChromeClient(); 153 | 154 | if(currentWebChromeClient != null && !currentWebChromeClient.getClass().isAssignableFrom(RobotiumWebClient.class)){ 155 | originalWebChromeClient = currentWebChromeClient; 156 | } 157 | robotiumWebCLient.enableJavascriptAndSetRobotiumWebClient(webViews, originalWebChromeClient); 158 | return getJavaScriptAsString(); 159 | } 160 | 161 | /** 162 | * Returns the current WebChromeClient through reflection 163 | * 164 | * @return the current WebChromeClient 165 | * 166 | */ 167 | 168 | private WebChromeClient getCurrentWebChromeClient(){ 169 | WebChromeClient currentWebChromeClient = null; 170 | 171 | Object currentWebView = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(WebView.class, true)); 172 | 173 | if (android.os.Build.VERSION.SDK_INT >= 16) { 174 | try{ 175 | currentWebView = new Reflect(currentWebView).field("mProvider").out(Object.class); 176 | }catch(IllegalArgumentException ignored) {} 177 | } 178 | 179 | try{ 180 | if (android.os.Build.VERSION.SDK_INT >= 19) { 181 | Object mClientAdapter = new Reflect(currentWebView).field("mContentsClientAdapter").out(Object.class); 182 | currentWebChromeClient = new Reflect(mClientAdapter).field("mWebChromeClient").out(WebChromeClient.class); 183 | } 184 | else { 185 | Object mCallbackProxy = new Reflect(currentWebView).field("mCallbackProxy").out(Object.class); 186 | currentWebChromeClient = new Reflect(mCallbackProxy).field("mWebChromeClient").out(WebChromeClient.class); 187 | } 188 | }catch(Exception ignored){} 189 | 190 | return currentWebChromeClient; 191 | } 192 | 193 | /** 194 | * Enters text into a web element using the given By method 195 | * 196 | * @param by the By object e.g. By.id("id"); 197 | * @param text the text to enter 198 | */ 199 | 200 | public void enterTextIntoWebElement(final By by, final String text){ 201 | if(by instanceof By.Id){ 202 | executeJavaScriptFunction("enterTextById(\""+by.getValue()+"\", \""+text+"\");"); 203 | } 204 | else if(by instanceof By.Xpath){ 205 | executeJavaScriptFunction("enterTextByXpath(\""+by.getValue()+"\", \""+text+"\");"); 206 | } 207 | else if(by instanceof By.CssSelector){ 208 | executeJavaScriptFunction("enterTextByCssSelector(\""+by.getValue()+"\", \""+text+"\");"); 209 | } 210 | else if(by instanceof By.Name){ 211 | executeJavaScriptFunction("enterTextByName(\""+by.getValue()+"\", \""+text+"\");"); 212 | } 213 | else if(by instanceof By.ClassName){ 214 | executeJavaScriptFunction("enterTextByClassName(\""+by.getValue()+"\", \""+text+"\");"); 215 | } 216 | else if(by instanceof By.Text){ 217 | executeJavaScriptFunction("enterTextByTextContent(\""+by.getValue()+"\", \""+text+"\");"); 218 | } 219 | else if(by instanceof By.TagName){ 220 | executeJavaScriptFunction("enterTextByTagName(\""+by.getValue()+"\", \""+text+"\");"); 221 | } 222 | } 223 | 224 | /** 225 | * Executes JavaScript determined by the given By object 226 | * 227 | * @param by the By object e.g. By.id("id"); 228 | * @param shouldClick true if click should be performed 229 | * @return true if JavaScript function was executed 230 | */ 231 | 232 | public boolean executeJavaScript(final By by, boolean shouldClick){ 233 | if(by instanceof By.Id){ 234 | return executeJavaScriptFunction("id(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 235 | } 236 | else if(by instanceof By.Xpath){ 237 | return executeJavaScriptFunction("xpath(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 238 | } 239 | else if(by instanceof By.CssSelector){ 240 | return executeJavaScriptFunction("cssSelector(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 241 | } 242 | else if(by instanceof By.Name){ 243 | return executeJavaScriptFunction("name(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 244 | } 245 | else if(by instanceof By.ClassName){ 246 | return executeJavaScriptFunction("className(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 247 | } 248 | else if(by instanceof By.Text){ 249 | return executeJavaScriptFunction("textContent(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 250 | } 251 | else if(by instanceof By.TagName){ 252 | return executeJavaScriptFunction("tagName(\""+by.getValue()+"\", \"" + String.valueOf(shouldClick) + "\");"); 253 | } 254 | return false; 255 | } 256 | 257 | /** 258 | * Executes the given JavaScript function 259 | * 260 | * @param function the function as a String 261 | * @return true if JavaScript function was executed 262 | */ 263 | 264 | private boolean executeJavaScriptFunction(final String function) { 265 | List webViews = viewFetcher.getCurrentViews(WebView.class, true); 266 | final WebView webView = viewFetcher.getFreshestView((ArrayList) webViews); 267 | 268 | if(webView == null) { 269 | return false; 270 | } 271 | 272 | final String javaScript = setWebFrame(prepareForStartOfJavascriptExecution(webViews)); 273 | 274 | inst.runOnMainSync(new Runnable() { 275 | public void run() { 276 | if(webView != null){ 277 | webView.loadUrl("javascript:" + javaScript + function); 278 | } 279 | } 280 | }); 281 | return true; 282 | } 283 | 284 | private String setWebFrame(String javascript){ 285 | String frame = config.webFrame; 286 | 287 | if(frame.isEmpty() || frame.equals("document")){ 288 | return javascript; 289 | } 290 | javascript = javascript.replaceAll(Pattern.quote("document, "), "document.getElementById(\""+frame+"\").contentDocument, "); 291 | javascript = javascript.replaceAll(Pattern.quote("document.body, "), "document.getElementById(\""+frame+"\").contentDocument, "); 292 | return javascript; 293 | } 294 | 295 | /** 296 | * Returns true if the view is sufficiently shown 297 | * 298 | * @param view the view to check 299 | * @return true if the view is sufficiently shown 300 | */ 301 | 302 | public final boolean isWebElementSufficientlyShown(WebElement webElement){ 303 | final WebView webView = viewFetcher.getFreshestView(viewFetcher.getCurrentViews(WebView.class, true)); 304 | final int[] xyWebView = new int[2]; 305 | 306 | if(webView != null && webElement != null){ 307 | webView.getLocationOnScreen(xyWebView); 308 | 309 | if(xyWebView[1] + webView.getHeight() > webElement.getLocationY()) 310 | return true; 311 | } 312 | return false; 313 | } 314 | 315 | /** 316 | * Splits a name by upper case. 317 | * 318 | * @param name the name to split 319 | * @return a String with the split name 320 | * 321 | */ 322 | 323 | public String splitNameByUpperCase(String name) { 324 | String [] texts = name.split("(?=\\p{Upper})"); 325 | StringBuilder stringToReturn = new StringBuilder(); 326 | 327 | for(String string : texts){ 328 | 329 | if(stringToReturn.length() > 0) { 330 | stringToReturn.append(" " + string.toLowerCase()); 331 | } 332 | else { 333 | stringToReturn.append(string.toLowerCase()); 334 | } 335 | } 336 | return stringToReturn.toString(); 337 | } 338 | 339 | /** 340 | * Returns the JavaScript file RobotiumWeb.js as a String 341 | * 342 | * @return the JavaScript file RobotiumWeb.js as a {@code String} 343 | */ 344 | 345 | private String getJavaScriptAsString() { 346 | InputStream fis = getClass().getResourceAsStream("RobotiumWeb.js"); 347 | StringBuffer javaScript = new StringBuffer(); 348 | 349 | try { 350 | BufferedReader input = new BufferedReader(new InputStreamReader(fis)); 351 | String line = null; 352 | while (( line = input.readLine()) != null){ 353 | javaScript.append(line); 354 | javaScript.append("\n"); 355 | } 356 | input.close(); 357 | } catch (IOException e) { 358 | throw new RuntimeException(e); 359 | } 360 | return javaScript.toString(); 361 | } 362 | } -------------------------------------------------------------------------------- /robotium-solo/src/main/java/com/robotium/solo/Zoomer.java: -------------------------------------------------------------------------------- 1 | package com.robotium.solo; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.SystemClock; 5 | import android.view.MotionEvent; 6 | import android.view.MotionEvent.PointerProperties; 7 | import android.view.MotionEvent.PointerCoords; 8 | import android.graphics.PointF; 9 | 10 | 11 | class Zoomer { 12 | 13 | private final Instrumentation _instrument; 14 | public static final int GESTURE_DURATION_MS = 1000; 15 | public static final int EVENT_TIME_INTERVAL_MS = 10; 16 | 17 | public Zoomer(Instrumentation inst) 18 | { 19 | this._instrument = inst; 20 | } 21 | 22 | public void generateZoomGesture(PointF startPoint1, PointF startPoint2, PointF endPoint1, PointF endPoint2) 23 | { 24 | 25 | long downTime = SystemClock.uptimeMillis(); 26 | long eventTime = SystemClock.uptimeMillis(); 27 | 28 | float startX1 = startPoint1.x; 29 | float startY1 = startPoint1.y; 30 | float startX2 = startPoint2.x; 31 | float startY2 = startPoint2.y; 32 | 33 | float endX1 = endPoint1.x; 34 | float endY1 = endPoint1.y; 35 | float endX2 = endPoint2.x; 36 | float endY2 = endPoint2.y; 37 | 38 | //pointer 1 39 | float x1 = startX1; 40 | float y1 = startY1; 41 | 42 | //pointer 2 43 | float x2 = startX2; 44 | float y2 = startY2; 45 | 46 | PointerCoords[] pointerCoords = new PointerCoords[2]; 47 | PointerCoords pc1 = new PointerCoords(); 48 | PointerCoords pc2 = new PointerCoords(); 49 | pc1.x = x1; 50 | pc1.y = y1; 51 | pc1.pressure = 1; 52 | pc1.size = 1; 53 | pc2.x = x2; 54 | pc2.y = y2; 55 | pc2.pressure = 1; 56 | pc2.size = 1; 57 | pointerCoords[0] = pc1; 58 | pointerCoords[1] = pc2; 59 | 60 | PointerProperties[] pointerProperties = new PointerProperties[2]; 61 | PointerProperties pp1 = new PointerProperties(); 62 | PointerProperties pp2 = new PointerProperties(); 63 | pp1.id = 0; 64 | pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 65 | pp2.id = 1; 66 | pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 67 | pointerProperties[0] = pp1; 68 | pointerProperties[1] = pp2; 69 | 70 | MotionEvent event; 71 | // send the initial touches 72 | event = MotionEvent.obtain( downTime, 73 | eventTime, 74 | MotionEvent.ACTION_DOWN, 75 | 1, 76 | pointerProperties, 77 | pointerCoords, 78 | 0, 0, // metaState, buttonState 79 | 1, // x precision 80 | 1, // y precision 81 | 0, 0, 0, 0 ); // deviceId, edgeFlags, source, flags 82 | _instrument.sendPointerSync(event); 83 | 84 | event = MotionEvent.obtain( downTime, 85 | eventTime, 86 | MotionEvent.ACTION_POINTER_DOWN + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 87 | 2, 88 | pointerProperties, 89 | pointerCoords, 90 | 0, 0, 91 | 1, 92 | 1, 93 | 0, 0, 0, 0 ); 94 | _instrument.sendPointerSync(event); 95 | 96 | int numMoves = GESTURE_DURATION_MS / EVENT_TIME_INTERVAL_MS; 97 | 98 | float stepX1 = (endX1 - startX1) / numMoves; 99 | float stepY1 = (endY1 - startY1) / numMoves; 100 | float stepX2 = (endX2 - startX2) / numMoves; 101 | float stepY2 = (endY2 - startY2) / numMoves; 102 | 103 | // send the zoom 104 | for (int i = 0; i < numMoves; i++) 105 | { 106 | eventTime += EVENT_TIME_INTERVAL_MS; 107 | pointerCoords[0].x += stepX1; 108 | pointerCoords[0].y += stepY1; 109 | pointerCoords[1].x += stepX2; 110 | pointerCoords[1].y += stepY2; 111 | 112 | event = MotionEvent.obtain( downTime, 113 | eventTime, 114 | MotionEvent.ACTION_MOVE, 115 | 2, 116 | pointerProperties, 117 | pointerCoords, 118 | 0, 0, 119 | 1, 120 | 1, 121 | 0, 0, 0, 0 ); 122 | _instrument.sendPointerSync(event); 123 | } 124 | } 125 | } 126 | --------------------------------------------------------------------------------