├── .gitignore ├── .whitesource ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── AndroidManifest.xml ├── aidl └── com │ └── bitbar │ └── testdroid │ └── aidl │ ├── IMetadataService.aidl │ └── IScreenshotService.aidl └── java └── com └── bitbar └── recorder └── extensions ├── Clicker.java ├── ExtSolo.java ├── HtmlUtils.java ├── Messages.java ├── OtherUtils.java ├── ProxyWebChromeClient.java ├── ScreenshotUtils.java └── Waiter.java /.gitignore: -------------------------------------------------------------------------------- 1 | res 2 | gen 3 | bin 4 | .idea 5 | target 6 | *.iml 7 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "configMode": "AUTO", 4 | "configExternalURL": "", 5 | "projectToken": "", 6 | "baseBranches": [] 7 | }, 8 | "checkRunSettings": { 9 | "vulnerableCheckRunConclusionLevel": "failure", 10 | "displayMode": "diff" 11 | }, 12 | "issueSettings": { 13 | "minSeverityLevel": "LOW" 14 | } 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Bitbar Technologies 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robotium Extensions 2 | 3 | Extension of robotium-solo library. Robotium ExtSolo extends Solo class and makes testing easier. 4 | ExtSolo is reporting executed steps to file metadata.json under /sdcard/test-screenshots 5 | when test is executed in Testdroid Cloud 6 | 7 | It requires robotium-solo included in the project. 8 | 9 | ## Usage 10 | It's as simply as replacing Solo with ExtSolo and changing initialization: 11 | 12 |
13 | ExtSolo solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass().getCanonicalName(), getName());
14 | 
15 | 16 | ## Build with maven: 17 | mvn clean install -Dandroid.sdk.path=path_to_sdk 18 | 19 | ## License 20 | 21 | See the [LICENSE](LICENSE) file. 22 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.testdroid 6 | robotium-extensions 7 | 5.3.2 8 | apk 9 | 10 | robotium-extensions 11 | http://maven.apache.org 12 | 13 | 14 | com.jayway.android.robotium 15 | robotium 16 | 5.6.3 17 | 18 | 19 | 20 | 4.4.1 21 | 22 | 23 | 24 | 25 | 26 | com.jayway.android.robotium 27 | robotium-solo 28 | ${parent.version} 29 | 30 | 31 | com.google.android 32 | android 33 | ${android.jar.version} 34 | 35 | 36 | com.google.android 37 | android-test 38 | ${android.jar.version} 39 | 40 | 41 | 42 | 43 | 44 | 45 | com.jayway.android.robotium 46 | robotium-solo 47 | 48 | 49 | com.google.android 50 | android 51 | provided 52 | 53 | 54 | com.google.android 55 | android-test 56 | provided 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | com.simpligility.maven.plugins 65 | android-maven-plugin 66 | ${android-maven-plugin.version} 67 | true 68 | 69 | 70 | 71 | 72 | 73 | com.simpligility.maven.plugins 74 | android-maven-plugin 75 | 76 | false 77 | 78 | 79 | 80 | 81 | generate-sources 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/aidl/com/bitbar/testdroid/aidl/IMetadataService.aidl: -------------------------------------------------------------------------------- 1 | package com.bitbar.testdroid.aidl; 2 | 3 | interface IMetadataService { 4 | void addAction(String name, String type, String currentActivity, String className, String methodName); 5 | void addDurationToAction(); 6 | void addScreenshotToMetadata(String name, boolean failed, int orientation); 7 | void saveMetadataFile(); 8 | void changeActionDescription(String name); 9 | void setErrorMessage(String message); 10 | void setAllActivitiesFromApplication(in List list); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/aidl/com/bitbar/testdroid/aidl/IScreenshotService.aidl: -------------------------------------------------------------------------------- 1 | package com.bitbar.testdroid.aidl; 2 | 3 | interface IScreenshotService { 4 | boolean takeScreenshot(String name); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/Clicker.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import android.content.res.Resources.NotFoundException; 4 | import android.view.View; 5 | import android.widget.EditText; 6 | import com.bitbar.recorder.extensions.OtherUtils.Type; 7 | 8 | class Clicker { 9 | 10 | private ExtSolo extSolo; 11 | 12 | Clicker(ExtSolo extSolo) { 13 | this.extSolo = extSolo; 14 | } 15 | 16 | void click( 17 | Class clazz, View view, Integer index, String text, boolean longClick, Integer time) { 18 | final View toClick = (view != null) ? view : (index != null) ? extSolo.getView(clazz, index) : null; 19 | 20 | StringBuilder sb = new StringBuilder(); 21 | sb.append("Click ").append(longClick ? "long on " : "on "); 22 | 23 | String[] parts = clazz.getName().split("\\."); 24 | 25 | sb.append(parts[parts.length - 1]); 26 | 27 | if (text != null) { 28 | sb.append(" with text: ").append(text); 29 | } else if (index != null) { 30 | sb.append(" with index: ").append(index); 31 | } else if (view != null) { 32 | if (view.getId() != View.NO_ID) { 33 | try { 34 | sb.append(" with id: ").append(view.getContext().getResources().getResourceEntryName(view.getId())); 35 | } catch (NotFoundException e) { 36 | sb.append(" with id: ").append(view.getId()); 37 | } 38 | } else if (view.getContentDescription() != null) { 39 | sb.append(" with description: ").append(view.getContentDescription()); 40 | } 41 | } 42 | extSolo.getOtherUtils().addAction(sb.toString(), Type.click); 43 | 44 | if (toClick != null && toClick instanceof EditText && view != null && view.hasFocusable() && !view.hasFocus()) { 45 | extSolo.getInstrumentation().runOnMainSync(new Runnable() { 46 | public void run() { 47 | toClick.requestFocus(); 48 | } 49 | }); 50 | } 51 | if (text == null) { 52 | extSolo.soloClick(toClick, longClick, time); 53 | } else { 54 | extSolo.clickOnTextBySolo(clazz, text); 55 | } 56 | extSolo.getOtherUtils().addDurationToAction(); 57 | } 58 | 59 | void click(Class clazz, View view, boolean longClick) { 60 | click(clazz, view, null, null, longClick, null); 61 | } 62 | 63 | void click(Class clazz, Integer index, boolean longClick) { 64 | click(clazz, null, index, null, longClick, null); 65 | } 66 | 67 | void click(Class clazz, String text, boolean longClick) { 68 | click(clazz, null, null, text, longClick, null); 69 | } 70 | 71 | void click(Class clazz, View view, boolean longClick, int time) { 72 | click(clazz, view, null, null, longClick, time); 73 | } 74 | 75 | void click(Class clazz, Integer index, boolean longClick, int time) { 76 | click(clazz, null, index, null, longClick, time); 77 | } 78 | 79 | void click(Class clazz, String text, boolean longClick, int time) { 80 | click(clazz, null, null, text, longClick, time); 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/HtmlUtils.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import android.app.Activity; 4 | import android.util.Log; 5 | import android.view.View; 6 | import android.webkit.WebView; 7 | import junit.framework.AssertionFailedError; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.lang.reflect.Field; 13 | import java.lang.reflect.Method; 14 | import java.util.ArrayList; 15 | import java.util.Map; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | import static junit.framework.Assert.assertTrue; 20 | 21 | class HtmlUtils { 22 | 23 | public static String JS_RESULT_PREFIX = "EXTOSOLO_JS_RESULT:"; 24 | 25 | private static final int TIMEOUT_WEB_VIEW_FULLY_LOADED = 10000; 26 | private static final int TIMEOUT_FOR_WEBVIEW = 60000; 27 | private static final int INTERVAL_FOR_CHECK = 500; 28 | private static final int TIMEOUT_FOR_JS_RESPONSE = 2000; 29 | 30 | private ExtSolo extSolo; 31 | 32 | private WebView webView; 33 | private String jsResult; 34 | private String methodName; 35 | private String className; 36 | private Object browserFrame; 37 | private Activity currentActivity; 38 | 39 | HtmlUtils(ExtSolo extSolo, String className, String methodName) { 40 | this.extSolo = extSolo; 41 | this.className = className; 42 | this.methodName = methodName; 43 | } 44 | 45 | public void setCurrentActivity(Activity activity) { 46 | this.currentActivity = activity; 47 | } 48 | 49 | public Activity getCurrentActivity() { 50 | if (currentActivity == null) { 51 | currentActivity = extSolo.getCurrentActivity(); 52 | } 53 | 54 | return currentActivity; 55 | } 56 | 57 | protected boolean elementExistsOnHtmlPage( 58 | String tag, String id, String name, String className, int index, String xPath, 59 | Map customAttributes, String selector) { 60 | return elementExistsOnHtmlPage(getWebView(), tag, id, name, className, index, xPath, customAttributes, selector); 61 | } 62 | 63 | protected boolean elementExistsOnHtmlPage( 64 | WebView webview, String tag, String id, String name, String className, int index, String xPath, 65 | Map customAttributes, String selector) { 66 | final String element = extSolo.getOtherUtils().isNullOrEmpty( 67 | selector) ? getHTMLElement(tag, id, name, className, 68 | index, xPath, customAttributes) : getHTMLElement(selector, index); 69 | return Boolean.valueOf(executeCommandWithResult(webview, element + " !== undefined")); 70 | } 71 | 72 | protected int numberOfHtmlElements( 73 | String tag, String id, String name, String className, String xPath, Map customAttributes, String selector) { 75 | final String element = extSolo.getOtherUtils().isNullOrEmpty(selector) ? getHTMLElement(tag, id, name, 76 | className, -1, xPath, customAttributes) : getHTMLElement(selector, -1); 77 | return Integer.valueOf(executeCommandWithResult(getWebView(), element + ".length")); 78 | } 79 | 80 | protected boolean waitForHtmlElement(String tag, String id, String name, 81 | String className, int index, int time, String xPath, 82 | Map customAttributes, String selector) { 83 | boolean result = false; 84 | WebView wv = getWebView(); 85 | 86 | //waiting needed time for element 87 | long timeOut = System.currentTimeMillis() + time; 88 | while (System.currentTimeMillis() <= timeOut && !(result = elementExistsOnHtmlPage(wv, tag, id, name, 89 | className, index, xPath, customAttributes, selector))) { 90 | extSolo.soloSleep(INTERVAL_FOR_CHECK); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | protected void enterTextIntoHtmlElement( 97 | String tag, String id, String name, String className, final String text, int index, String xPath, 98 | Map customAttributes, String selector) { 99 | WebView wv = getWebView(); 100 | assertTrue(Messages.ELEMENT_DOES_NOT_EXIST, elementExistsOnHtmlPage(wv, tag, id, name, className, index, 101 | xPath, customAttributes, selector)); 102 | final String element = extSolo.getOtherUtils().isNullOrEmpty(selector) ? 103 | getHTMLElement(tag, id, name, className, index, xPath, customAttributes) : 104 | getHTMLElement(selector, index); 105 | 106 | executeCommand( 107 | wv, 108 | "var el = " + element + ";" + 109 | "el.value = '" + text + "';" + 110 | "var changeEvent = document.createEvent(\"HTMLEvents\");" + 111 | "changeEvent.initEvent(\"change\",true);" + 112 | "el.dispatchEvent(changeEvent);" + 113 | "var keydownEvent = document.createEvent(\"UIEvents\");" + 114 | "keydownEvent.initEvent(\"keydown\",true);" + 115 | "el.dispatchEvent(keydownEvent);" + 116 | "var keypressEvent = document.createEvent(\"UIEvents\");" + 117 | "keypressEvent.initEvent(\"keypress\",true);" + 118 | "el.dispatchEvent(keypressEvent);" + 119 | "var keyupEvent = document.createEvent(\"UIEvents\");" + 120 | "keyupEvent.initEvent(\"keyup\",true);" + 121 | "el.dispatchEvent(keyupEvent);"); 122 | } 123 | 124 | protected void clickOnHtmlElement( 125 | final String tag, String id, String name, String className, int index, String xPath, Map customAttributes, String selector) { 127 | WebView wv = getWebView(); 128 | assertTrue(Messages.ELEMENT_DOES_NOT_EXIST, elementExistsOnHtmlPage(wv, tag, id, name, className, index, xPath, customAttributes, selector)); 129 | final String element = extSolo.getOtherUtils().isNullOrEmpty( 130 | selector) ? getHTMLElement(tag, id, name, className, 131 | index, xPath, customAttributes) : getHTMLElement(selector, index); 132 | 133 | Log.d(ExtSolo.TAG, "Clicking element " + element); 134 | executeCommand( 135 | wv, 136 | "var elem =" + element + "; " + 137 | 138 | "var focus = document.createEvent(\"HTMLEvents\");" + 139 | "focus.initEvent(\"focus\",true);" + 140 | "elem.dispatchEvent(focus);" + 141 | 142 | "var mousedown = document.createEvent(\"MouseEvents\");" + 143 | "mousedown.initMouseEvent(\"mousedown\",true);" + 144 | "elem.dispatchEvent(mousedown);" + 145 | 146 | "var posX = elem.offsetLeft + 10;" + 147 | "var posY = elem.offsetTop + 10;" + 148 | "var identifier = Date.now();" + 149 | "var touch = document.createTouch(window,elem,identifier,posX,posY,posX,posY);" + 150 | "var touchList = document.createTouchList(touch);" + 151 | 152 | "var touchstart = document.createEvent(\"TouchEvent\");" + 153 | "touchstart.initTouchEvent(touchList, touchList, touchList, 'touchstart', window, posX, posY, posX, posY, false, false, false, false);" + 154 | "elem.dispatchEvent(touchstart);" + 155 | 156 | "var mouseup = document.createEvent(\"MouseEvents\");" + 157 | "mouseup.initMouseEvent(\"mouseup\",true);" + 158 | "elem.dispatchEvent(mouseup);" + 159 | 160 | "var touchend = document.createEvent(\"TouchEvent\");" + 161 | "touchend.initTouchEvent(touchList, touchList, touchList, 'touchend', window, posX, posY, posX, posY, false, false, false, false);" + 162 | "elem.dispatchEvent(touchend);" + 163 | 164 | "var click = document.createEvent(\"MouseEvents\");" + 165 | "click.initMouseEvent(\"click\",true);" + 166 | "elem.dispatchEvent(click);"); 167 | } 168 | 169 | private Object getBrowserFrame() { 170 | try { 171 | if (webView != null) { 172 | // getting WebViewCore from right WebViewProvider - WebView or 173 | // WebViewClassic 174 | @SuppressWarnings("rawtypes") 175 | Class clazz = webView.getClass(); 176 | if (!clazz.getCanonicalName().equals( 177 | "android.webkit.WebViewClassic") 178 | && !clazz.getCanonicalName().equals("android.webkit.WebView")) { 179 | try { 180 | while (!clazz.equals(Class.forName("android.webkit.WebView"))) { 181 | clazz = clazz.getSuperclass(); 182 | Log.d(ExtSolo.TAG, "WebView class: " + clazz.getName()); 183 | } 184 | } catch (ClassNotFoundException e) { 185 | Log.w(ExtSolo.TAG, e); 186 | } 187 | } 188 | 189 | //normally webView is suffice but in SDK 4.1 whole functionality 190 | //was moved to WebViewProvider interface which is implemented 191 | //by webViewClassic 192 | Object webViewProvider = getWebViewOrWebViewClassic(clazz, webView); 193 | clazz = webViewProvider.getClass().getCanonicalName().equals("android.webkit.WebViewClassic") ? 194 | webViewProvider.getClass() : clazz; 195 | 196 | Method getWebViewCore = clazz.getDeclaredMethod("getWebViewCore"); 197 | getWebViewCore.setAccessible(true); 198 | Object webViewCore = getWebViewCore.invoke(webViewProvider); 199 | 200 | //getting BrowserFrame 201 | Method getBrowserFrame = webViewCore.getClass().getDeclaredMethod("getBrowserFrame"); 202 | getBrowserFrame.setAccessible(true); 203 | Object browserFrame = getBrowserFrame.invoke(webViewCore); 204 | 205 | return browserFrame; 206 | } else { 207 | return null; 208 | } 209 | } catch (Exception e) { 210 | Log.w(ExtSolo.TAG, e); 211 | } 212 | 213 | return null; 214 | } 215 | 216 | private Object getWebViewOrWebViewClassic(@SuppressWarnings("rawtypes") Class webViewClass, Object webView) throws IllegalArgumentException, IllegalAccessException { 217 | try { 218 | Field mProvider = webViewClass.getDeclaredField("mProvider"); 219 | mProvider.setAccessible(true); 220 | 221 | return mProvider.get(webView); 222 | } catch (NoSuchFieldException e) { 223 | //it means there is no WebViewClassic so we use 224 | //SDK below 4.1 and WebView has whole needed functionality 225 | return webView; 226 | } 227 | } 228 | 229 | private String getTextFromBrowserFrame(Object browserFrame) { 230 | try { 231 | Method documentAsText = browserFrame.getClass().getDeclaredMethod("externalRepresentation"); 232 | documentAsText.setAccessible(true); 233 | 234 | Object website = documentAsText.invoke(browserFrame); 235 | 236 | return String.valueOf(website); 237 | } catch (Exception e) { 238 | Log.w(ExtSolo.TAG, e); 239 | } 240 | 241 | return ""; 242 | } 243 | 244 | protected boolean isTextPresentOnHtmlPage(String text) { 245 | boolean textPresented = false; 246 | 247 | //it's done to be sure that page is fully loaded and browserFrame is not null 248 | getWebView(); 249 | 250 | // get only text in "" 251 | Pattern p = Pattern.compile("\".*\""); 252 | String result = ""; 253 | Matcher m = p.matcher(getTextFromBrowserFrame(browserFrame)); 254 | while (m.find()) { 255 | result += m.group(0); 256 | } 257 | 258 | // change coding 259 | String s[] = String.valueOf(result).split("\\\\x\\{"); 260 | for (int i = 0; i < s.length; i++) { 261 | int count = s[i].indexOf("}"); 262 | switch (count) { 263 | case 1: 264 | s[i] = "\\u000" + s[i].replaceFirst("\\}", ""); 265 | break; 266 | case 2: 267 | s[i] = "\\u00" + s[i].replaceFirst("\\}", ""); 268 | break; 269 | case 3: 270 | s[i] = "\\u0" + s[i].replaceFirst("\\}", ""); 271 | break; 272 | case 4: 273 | s[i] = "\\u" + s[i].replaceFirst("\\}", ""); 274 | break; 275 | } 276 | } 277 | result = ""; 278 | for (String value : s) { 279 | result += value; 280 | } 281 | 282 | // convert unicode to utf-8 283 | Pattern p2 = Pattern.compile("\\\\u([0-9A-F]{4})"); 284 | Matcher m2 = p2.matcher(result); 285 | while (m2.find()) { 286 | result = result.replaceAll("\\" + m2.group(0), Character.toString((char) Integer.parseInt(m2.group(1), 16))); 287 | } 288 | 289 | // find text 290 | Pattern p3 = Pattern.compile(text); 291 | Matcher m3 = p3.matcher(result); 292 | while (m3.find()) { 293 | textPresented = true; 294 | } 295 | return textPresented; 296 | } 297 | 298 | protected void htmlZoomIn() { 299 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 300 | public void run() { 301 | getWebView().zoomIn(); 302 | } 303 | }); 304 | } 305 | 306 | protected void htmlZoomOut() { 307 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 308 | public void run() { 309 | getWebView().zoomOut(); 310 | } 311 | }); 312 | } 313 | 314 | protected void htmlGoForward() { 315 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 316 | public void run() { 317 | getWebView().goForward(); 318 | } 319 | }); 320 | } 321 | 322 | protected void htmlGoBack() { 323 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 324 | public void run() { 325 | getWebView().goBack(); 326 | } 327 | }); 328 | } 329 | 330 | protected synchronized void setJSResult(Object result) { 331 | this.jsResult = result.toString(); 332 | } 333 | 334 | protected synchronized String getJSResult() { 335 | return this.jsResult; 336 | } 337 | 338 | protected String getHtmlCode() { 339 | return String.valueOf(executeCommandWithResult(getWebView(), "''+document.getElementsByTagName('html')[0].innerHTML+''")); 340 | } 341 | 342 | protected void injectJavaScriptFile(String fileName) throws IOException { 343 | injectJavaScriptCode(getJavaScriptFromFile(fileName)); 344 | } 345 | 346 | protected void injectJavaScriptCode(final String code) { 347 | getCurrentActivity().runOnUiThread(new Runnable() { 348 | public void run() { 349 | getWebView().loadUrl("javascript:" + code); 350 | } 351 | }); 352 | } 353 | 354 | private WebView getWebView() { 355 | try { 356 | boolean found = false; 357 | WebView currentWebView = null; 358 | long timeOut = System.currentTimeMillis() + TIMEOUT_FOR_WEBVIEW; 359 | 360 | //searching first WebView on the screen 361 | while (System.currentTimeMillis() <= timeOut && !found) { 362 | ArrayList views = extSolo.getViews(); 363 | for (View v : views) { 364 | if (v instanceof WebView) { 365 | found = true; 366 | currentWebView = (WebView) v; 367 | } 368 | } 369 | extSolo.soloSleep(INTERVAL_FOR_CHECK); 370 | } 371 | 372 | //checking if it's new web view or already cached 373 | //if new then JavaScriptInterface need to be loaded 374 | //and webView need to be reloaded to load JSI 375 | //and then WebView is cached 376 | if (webView == null || webView != currentWebView) { 377 | webView = currentWebView; 378 | //we need to be sure that any WebView exists 379 | assertTrue("There is no WebView", webView != null); 380 | browserFrame = getBrowserFrame(); 381 | 382 | getCurrentActivity().runOnUiThread(new Runnable() { 383 | public void run() { 384 | webView.getSettings().setJavaScriptEnabled(true); 385 | webView.setWebChromeClient(new ProxyWebChromeClient(webView, HtmlUtils.this)); 386 | } 387 | }); 388 | } 389 | } catch (AssertionFailedError e) { 390 | String packageName = extSolo.getInstrumentation().getContext().getPackageName(); 391 | extSolo.fail(String.format("%s.%s.%s_src_fail", packageName, className, methodName), e); 392 | } 393 | 394 | return webView; 395 | } 396 | 397 | private void waitForWebViewFullyLoaded() { 398 | boolean result = false; 399 | //we need to check till webView is fully loaded(its content) 400 | //before do any loadUrl on it to avoid breaking our JS methods 401 | //during loading page 402 | long timeOut = System.currentTimeMillis() + TIMEOUT_WEB_VIEW_FULLY_LOADED; 403 | while (System.currentTimeMillis() <= timeOut && !(result = isWebViewFullyLoaded())) { 404 | extSolo.soloSleep(INTERVAL_FOR_CHECK); 405 | } 406 | 407 | if (!result) { 408 | Log.w(ExtSolo.TAG, Messages.WEBVIEW_NOT_FULLY_LOADED); 409 | } 410 | } 411 | 412 | private boolean isWebViewFullyLoaded() { 413 | if (browserFrame != null) { 414 | try { 415 | Field mCommitted = browserFrame.getClass().getDeclaredField("mCommitted"); 416 | mCommitted.setAccessible(true); 417 | 418 | Field mFirstLayoutDone = browserFrame.getClass().getDeclaredField("mFirstLayoutDone"); 419 | mFirstLayoutDone.setAccessible(true); 420 | 421 | return mCommitted.getBoolean(browserFrame) && mFirstLayoutDone.getBoolean(browserFrame); 422 | } catch (NoSuchFieldException e) { 423 | Log.w(ExtSolo.TAG, e); 424 | return false; 425 | } catch (IllegalAccessException e) { 426 | Log.w(ExtSolo.TAG, e); 427 | return false; 428 | } 429 | } else { 430 | return false; 431 | } 432 | } 433 | 434 | private String executeCommandWithResult(final WebView webView, final String command) { 435 | executeCommand(webView, String.format("console.log('%s' + (%s));", JS_RESULT_PREFIX, command)); 436 | 437 | setJSResult(false); 438 | synchronized (this) { 439 | try { 440 | this.wait(TIMEOUT_FOR_JS_RESPONSE); 441 | } catch (InterruptedException e) { 442 | Log.w(ExtSolo.TAG, e); 443 | } 444 | } 445 | 446 | return getJSResult(); 447 | } 448 | 449 | private void executeCommand(final WebView webView, final String command) { 450 | //check if webView exist to run script on it 451 | assertTrue(Messages.NO_WEBVIEW, webView != null); 452 | waitForWebViewFullyLoaded(); 453 | 454 | getCurrentActivity().runOnUiThread(new Runnable() { 455 | public void run() { 456 | webView.loadUrl("javascript:(function() { " + command + " })()"); 457 | } 458 | }); 459 | } 460 | 461 | private String getSelector( 462 | String tag, String id, String name, String className, Map customAttributes) { 463 | StringBuilder sB = new StringBuilder(); 464 | // START QUERY + TAG 465 | sB.append("document.querySelectorAll('").append(tag); 466 | //NAME 467 | if (!extSolo.getOtherUtils().isNullOrEmpty(name)) { 468 | sB.append("[name=").append(name).append("]"); 469 | } 470 | //ATTRIBUTES 471 | if (customAttributes != null) { 472 | for (int i = 0; i < customAttributes.size(); i++) { 473 | String key = customAttributes.keySet().toArray()[i].toString(); 474 | sB.append("[").append(key).append("=").append(customAttributes.get(key)).append("]"); 475 | } 476 | } 477 | //ID 478 | if (!extSolo.getOtherUtils().isNullOrEmpty(id)) { 479 | sB.append("#").append(id); 480 | } 481 | // CLASS 482 | if (!extSolo.getOtherUtils().isNullOrEmpty(className)) { 483 | String[] classes = className.split(" "); 484 | for (String aClass : classes) { 485 | if (aClass.length() > 0) { 486 | sB.append(".").append(aClass); 487 | } 488 | } 489 | } 490 | //END QUERY 491 | sB.append("')"); 492 | return sB.toString(); 493 | } 494 | 495 | private String getHTMLElement( 496 | String tag, String id, String name, String className, int index, String xPath, Map customAttributes) { 498 | StringBuilder sB = new StringBuilder(); 499 | if (extSolo.getOtherUtils().isNullOrEmpty(xPath)) { 500 | sB.append(getSelector(tag, id, name, className, customAttributes)); 501 | //INDEX 502 | if (index >= 0) { 503 | sB.append("[" + index + "]"); 504 | } 505 | } else { 506 | sB.append("document.evaluate(\"" + xPath + "\", document, null, 0, null).iterateNext()"); 507 | } 508 | return sB.toString(); 509 | } 510 | 511 | private String getHTMLElement(String selector, int index) { 512 | return String.format("document.querySelectorAll('%s')%s", selector, index < 0 ? "" : "[" + index + "]"); 513 | } 514 | 515 | private String getJavaScriptFromFile(String jsFileName) throws IOException { 516 | InputStream is = extSolo.getInstrumentation().getContext().getAssets().open(jsFileName); 517 | byte[] buffer = new byte[is.available()]; 518 | is.read(buffer); 519 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 520 | os.write(buffer); 521 | os.close(); 522 | is.close(); 523 | 524 | return os.toString(); 525 | } 526 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/Messages.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | public class Messages { 4 | public static final String ASSERT_CURRENT_ACTIVITY = "Assert current activity: %s"; 5 | public static final String ASSERT_MEMORY_NOT_LOW = "Assert memory not low"; 6 | public static final String CHANGE_ACTIVITY_TO = "Change activity to: %s"; 7 | public static final String CHANGE_LANGUAGE_TO_ONE_PARAM = "Change language to: \"%s\""; 8 | public static final String CHANGE_LANGUAGE_TO_TWO_PARAMS = "Change language to: \"%s, %s\""; 9 | public static final String CLEAR_EDIT_TEXT = "Clear edit text"; 10 | public static final String CLEAR_EDIT_TEXT_WITH_INDEX = "Clear edit text with index: %d"; 11 | public static final String CLICK_IN_LIST = "Click in list (line: %d)"; 12 | public static final String CLICK_IN_LIST_WITH_TEXT = "Click in list \"%s\" (line: %d)"; 13 | public static final String CLICK_LONG_ON_SCREEN = "Click long on screen: (%d, %d)"; 14 | public static final String CLICK_ON_HTML_ELEMENT_BY_CSS_SELECTOR = "Click on html element: %s with index %s"; 15 | public static final String CLICK_ON_HTML_ELEMENT_TAG = "Click on html element <%s id='%s' name='%s' class='%s' />"; 16 | public static final String CLICK_ON_HTML_ELEMENT_XPATH = "Click on html element, xPath: %s"; 17 | public static final String CLICK_ON_WEB_ELEMENT_S = "Click on WebElement: %s"; 18 | public static final String CLICK_ON_WEB_ELEMENT_S_I = "Click on WebElement: %s[%d]"; 19 | public static final String CLICK_ON_WEB_ELEMENT_5S = "Click on WebElement <%s id='%s' name='%s' class='%s' text='%s'/>"; 20 | public static final String CLICK_ON_MENU_ITEM = "Click on menu item \"%s\""; 21 | public static final String CLICK_ON_SCREEN = "Click on screen: (%d, %d)"; 22 | public static final String CONNECTED_TO_SCREENSHOT_SERVICE = "Connected to monitor service. Taking screenshot using ScreenshotService"; 23 | public static final String COULDNT_CALL_SCREENSHOT_SERVICE_METHOD = "Couldn't call remote method to ScreenshotService."; 24 | public static final String CURRENT_LOCALE = "Current locale: %s"; 25 | public static final String DRAG = "Drag from (%d, %d) to (%d, %d)"; 26 | public static final String ELEMENT_DOES_NOT_EXIST = "Element does not exist"; 27 | public static final String ELEMENT_EXISTS_ON_HTML_PAGE_BY_CSS_SELECTOR = "Element exists on html page: %s with index %s"; 28 | public static final String ELEMENT_EXISTS_ON_HTML_PAGE_TAG = "Element exists on html page <%s id='%s' name='%s' class='%s' />"; 29 | public static final String ELEMENT_EXISTS_ON_HTML_PAGE_XPATH = "Element exists on html page, xPath: %s"; 30 | public static final String ENTER_TEXT = "Enter text \"%s\""; 31 | public static final String ENTER_TEXT_BY_CSS_SELECTOR = "Enter text: \"%s\" into html element: %s with index %s"; 32 | public static final String ENTER_TEXT_TAG = "Enter text: \"%s\" into html element <%s id='%s' name='%s' class='%s' />"; 33 | public static final String ENTER_TEXT_XPATH = "Enter text: \"%s\" into html element, xPath: %s"; 34 | public static final String ENTER_TEXT_IN_WEB_ELEMENT = "Enter text: \"%s\" in WebElement: %s"; 35 | public static final String FIND_VIEW_BY_ID = "Find view by id: \"%s\""; 36 | public static final String GET_EDIT_TEXT = "Get edit text: \"%s\""; 37 | public static final String GET_HTML_CODE = "Get html code"; 38 | public static final String GET_TEXT = "Get text: \"%s\""; 39 | public static final String GET_VIEW = "Get view by index: %d"; 40 | public static final String GET_VIEW_BY_TEXT = "Get view by text: %s"; 41 | public static final String GO_BACK = "Go back"; 42 | public static final String HIDE_KEYBOARD = "Hide keyboard"; 43 | public static final String HTML_GO_BACK = "Html go back"; 44 | public static final String HTML_GO_FORWARD = "Html go forward"; 45 | public static final String HTML_ZOOM_IN = "Html zoom in"; 46 | public static final String HTML_ZOOM_OUT = "Html zoom out"; 47 | public static final String INJECT_JAVASCRIPT_CODE = "inject JavaScript code"; 48 | public static final String INJECT_JAVASCRIPT_FILE = "Inject JavaScript file"; 49 | public static final String IS_TEXT_PRESENT_ON_HTML_PAGE = "Is \"%s\" present on html page"; 50 | public static final String LOG_INFO_IN_LOGCAT = "Log info in LogCat, tag: \"%s\", msg: \"%s\""; 51 | public static final String METADATA_SERVICE_ERROR_ADD_ACTION = "Couldn't call remote method (addAction) to MetadataService."; 52 | public static final String METADATA_SERVICE_ERROR_ADD_DURATION = "Couldn't call remote method (addDurationToAction) to MetadataService."; 53 | public static final String METADATA_SERVICE_ERROR_ADD_SCREENSHOT = "Couldn't call remote method (addScreenshotToMetadata) to MetadataService."; 54 | public static final String METADATA_SERVICE_ERROR_CHANGE = "Couldn't call remote method (changeActionDescription) to MetadataService."; 55 | public static final String METADATA_SERVICE_ERROR_MSG_OR_DURATION = "Couldn't call remote method (setErrorMessage or addDurationToAction) to MetadataService."; 56 | public static final String METADATA_SERVICE_ERROR_SAVE = "Couldn't call remote method (saveMetadataFile) to MetadataService."; 57 | public static final String METADATA_SERVICE_ERROR_SET_ALL_ACTIVITIES = "Couldn't call remote method (setAllActivitiesFromApplication) to MetadataService."; 58 | public static final String MOCK_LOCATION_NO_PERMISSION = "Mocking location is not possible, probably caused by lack of permission"; 59 | public static final String MOVE_GALLERY = "Move gallery on %s"; 60 | public static final String MULTIPATH_DRAG = "Multipath drag"; 61 | public static final String NO_WEBVIEW = "There is no WebView"; 62 | public static final String NUMBER_OF_HTML_ELEMENTS_BY_CSS_SELECTOR = "Number of elements on html page: %s"; 63 | public static final String NUMBER_OF_HTML_ELEMENTS_TAG = "Number of elements on html page <%s id='%s' name='%s' class='%s' />"; 64 | public static final String NUMBER_OF_HTML_ELEMENTS_XPATH = "Number of elements on html page, xPath: %s"; 65 | public static final String SCREENSHOT_DRAWING_CACHE_NULL = "Screenshot couldn't be taken. Drawing cache from view is null."; 66 | public static final String SCREENSHOT_JOB_NOTIFIED = "Screenshot job notified"; 67 | public static final String SCREENSHOT_JOB_WOKE_UP = "Screenshot job woke up"; 68 | public static final String SCREENSHOT_NO_PERMISSION = "Unable to write screenshot %s - please make sure your application has permissions to write to external storage"; 69 | public static final String SCREENSHOT_SAVED_WITH_OLD_METHOD = "Screenshot saved using old method"; 70 | public static final String SCREENSHOT_SERVICE_FAILED = "Taking screenshot by ScreenshotService failed. Local method will be used"; 71 | public static final String SCREENSHOT_WAIT_INTERRUPTED = "Wait interrupted"; 72 | public static final String SCROLL_LIST_TO_LINE = "Scroll list to line %d"; 73 | public static final String SCROLL_LIST_WITH_INDEX_TO_LINE = "Scroll list with index %d to line %d"; 74 | public static final String SCROLL_TO_BOTTOM = "Scroll to bottom"; 75 | public static final String SCROLL_TO_TOP = "Scroll to top"; 76 | public static final String SCRRENSHOT_NO_VIEWS = "Screenshot couldn't be taken. There are no views on the screen"; 77 | public static final String SEARCH_TEXT = "Search text: \"%s\""; 78 | public static final String SEND_KEY_CHAR = "Send key: %c"; 79 | public static final String SEND_KEY_STRING = "Send key: \"%s\""; 80 | public static final String SET_DATE_PICKER = "Set date picker (%2d:%2d:%4d)"; 81 | public static final String SET_GPS_MOCK_LOCATION = "Set GPS mock location (%f, %f, %f)"; 82 | public static final String SET_ORIENTATION = "Set orientation \"%s\""; 83 | public static final String SET_TIME_PICKER = "Set time picker (%2d:%2d)"; 84 | public static final String SLEEP = "Sleep: %d"; 85 | public static final String TURN_WIRELESS_CONNECTION = "Turn %s wireless connection"; // on/off 86 | public static final String UNABLE_TO_CLOSE_OPENED_STREAM = "Unable to close opened FileInputStream"; 87 | public static final String WAIT_FOR_ACTIVITY_FAILED = "Wait for activity %s failed"; 88 | public static final String WAIT_FOR_HTML_ELEMENT_BY_CSS_SELECTOR = "Wait for html element: %s"; 89 | public static final String WAIT_FOR_HTML_ELEMENT_TAG = "Wait for html element <%s id='%s' name='%s' class='%s' />"; 90 | public static final String WAIT_FOR_HTML_ELEMENT_XPATH = "Wait for html element, xPath: %s"; 91 | public static final String WAIT_FOR_WEB_ELEMENT_S = "Wait for WebElement: %s"; 92 | public static final String WAIT_FOR_LOG_MESSAGE = "Wait for log message."; 93 | public static final String WAIT_FOR_TEXT = "Wait for \"%s\""; 94 | public static final String WAIT_FOR_VIEW = "Wait for %s"; 95 | public static final String WAIT_FOR_WIRELESS_CONNECTION = "Wait for wireless connection: %s"; 96 | public static final String WEBVIEW_NOT_FULLY_LOADED = "WebView is not fully loaded before next webView commands"; 97 | public static final String PRESS_SPINNER_ITEM = "Press spinner (index: %d, item index: %d)"; 98 | public static final String ORIGINAL_WEB_CHROME_CLIENT = "Original WebChromeClient: %s"; 99 | public static final String CANNOT_GET_ORIGINAL_WEB_CHROME_CLIENT = "Couldn't get original webChromeClient from WebView"; 100 | public static final String SEND_STRING = "Send string to application: %s"; 101 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/OtherUtils.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.content.pm.ActivityInfo; 9 | import android.content.pm.PackageManager; 10 | import android.content.pm.PackageManager.NameNotFoundException; 11 | import android.content.res.Configuration; 12 | import android.content.res.Resources.NotFoundException; 13 | import android.location.Location; 14 | import android.location.LocationManager; 15 | import android.net.wifi.WifiManager; 16 | import android.os.IBinder; 17 | import android.os.RemoteException; 18 | import android.os.SystemClock; 19 | import android.util.Log; 20 | import android.view.*; 21 | import android.view.inputmethod.InputMethodManager; 22 | import android.widget.*; 23 | import com.bitbar.recorder.extensions.ExtSolo.WIFI_STATE; 24 | import com.bitbar.testdroid.aidl.IMetadataService; 25 | 26 | import java.lang.reflect.Field; 27 | import java.lang.reflect.InvocationTargetException; 28 | import java.lang.reflect.Method; 29 | import java.util.*; 30 | 31 | class OtherUtils { 32 | 33 | private ExtSolo extSolo; 34 | 35 | private boolean mBound; 36 | private boolean mMetadataConnected; 37 | IMetadataService mMetadataService; 38 | 39 | private String methodName; 40 | private String className; 41 | private LocationManager locationManager = null; 42 | private Timer mockGPSTimer = null; 43 | 44 | private final Locale locale; 45 | 46 | private Integer screenWidth = null; 47 | private Integer screenHeight = null; 48 | private Display defaultDisplay = null; 49 | private static final int INTERVAL_FOR_CHECK = 500; 50 | 51 | protected enum Type { 52 | click, config, input, drag, assertion, wait, scroll, navigation, util 53 | } 54 | 55 | private ServiceConnection mConnection = new ServiceConnection() { 56 | public void onServiceDisconnected(ComponentName name) { 57 | mBound = false; 58 | mMetadataService = null; 59 | } 60 | 61 | public void onServiceConnected(ComponentName name, IBinder service) { 62 | mMetadataService = IMetadataService.Stub.asInterface(service); 63 | mBound = true; 64 | } 65 | }; 66 | 67 | OtherUtils(ExtSolo extSolo, String className, String methodName) { 68 | mMetadataConnected = extSolo.getInstrumentation() 69 | .getTargetContext() 70 | .bindService(new Intent("com.bitbar.testdroid.monitor.MetadataService") 71 | .setPackage("com.bitbar.testdroid.monitor"), mConnection, Context.BIND_AUTO_CREATE); 72 | 73 | //set needed variables 74 | this.extSolo = extSolo; 75 | this.className = className; 76 | this.methodName = methodName; 77 | 78 | //get current language 79 | locale = extSolo.getInstrumentation().getTargetContext().getResources().getConfiguration().locale; 80 | Log.d(ExtSolo.TAG, String.format(Messages.CURRENT_LOCALE, locale.toString())); 81 | 82 | //initializaion of mocking location - move to constructor 83 | try { 84 | locationManager = (LocationManager) extSolo.getInstrumentation().getContext().getSystemService(Context.LOCATION_SERVICE); 85 | locationManager.addTestProvider(LocationManager.GPS_PROVIDER, false, false, false, false, true, false, false, 1, 1); 86 | locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true); 87 | mockGPSTimer = new Timer(); 88 | } catch (SecurityException e) { 89 | locationManager = null; 90 | } 91 | 92 | setUp(); 93 | } 94 | 95 | protected boolean isNullOrEmpty(String text) { 96 | return text == null || text.equals(""); 97 | } 98 | 99 | protected void scrollListToLine(final ListView listView, final int line) { 100 | extSolo.getInstrumentation().runOnMainSync(new Runnable() { 101 | public void run() { 102 | listView.setSelection(line); 103 | } 104 | }); 105 | } 106 | 107 | protected Display getDefaultDisplay() { 108 | if (defaultDisplay == null) { 109 | defaultDisplay = extSolo.getCurrentActivity().getWindowManager().getDefaultDisplay(); 110 | } 111 | 112 | return defaultDisplay; 113 | } 114 | 115 | protected int getScreenWidth() { 116 | 117 | if (screenWidth == null) { 118 | screenWidth = (getDefaultDisplay().getOrientation() == ExtSolo.ORIENTATION_LANDSCAPE) ? 119 | getDefaultDisplay().getHeight() : getDefaultDisplay().getWidth(); 120 | } 121 | return screenWidth; 122 | } 123 | 124 | protected int getScreenHeight() { 125 | if (screenHeight == null) { 126 | screenHeight = (getDefaultDisplay().getOrientation() == ExtSolo.ORIENTATION_LANDSCAPE) ? 127 | getDefaultDisplay().getWidth() : getDefaultDisplay().getHeight(); 128 | } 129 | return screenHeight; 130 | } 131 | 132 | protected void waitForAnyView() { 133 | while (true) { 134 | if (extSolo.getViews().size() > 0) { 135 | return; 136 | } 137 | } 138 | } 139 | 140 | protected void waitForAnyListView() { 141 | while (true) { 142 | if (extSolo.getCurrentListViews().size() > 0) { 143 | return; 144 | } 145 | } 146 | } 147 | 148 | protected void hideKeyboard(final EditText editText) { 149 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 150 | public void run() { 151 | InputMethodManager imm = (InputMethodManager) extSolo 152 | .getInstrumentation().getTargetContext() 153 | .getSystemService(Context.INPUT_METHOD_SERVICE); 154 | imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); 155 | } 156 | }); 157 | } 158 | 159 | protected void hideKeyboard() { 160 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 161 | public void run() { 162 | extSolo.getCurrentActivity() 163 | .getWindow() 164 | .setSoftInputMode( 165 | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 166 | } 167 | }); 168 | } 169 | 170 | protected View findViewById(String id) { 171 | return findViewById(extSolo.getCurrentActivity().getResources() 172 | .getIdentifier(id.replaceAll("\\.R\\.id\\.", ":id/"), null, null)); 173 | } 174 | 175 | protected View findViewById(int id) { 176 | View view = extSolo.getView(id); 177 | if (view != null) 178 | return view; 179 | 180 | ArrayList views = extSolo.getViews(); 181 | for (View v : views) { 182 | if (v.getId() == id) { 183 | return v; 184 | } 185 | } 186 | return null; 187 | } 188 | 189 | protected void drag(float fromX, float toX, float fromY, float toY, int stepCount, int elapsed) { 190 | long interval = elapsed / stepCount; 191 | 192 | long downTime = SystemClock.uptimeMillis(); 193 | long eventTime = SystemClock.uptimeMillis(); 194 | float y = fromY; 195 | float x = fromX; 196 | float yStep = (toY - fromY) / stepCount; 197 | float xStep = (toX - fromX) / stepCount; 198 | MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, fromX, fromY, 0); 199 | try { 200 | extSolo.getInstrumentation().sendPointerSync(event); 201 | } catch (SecurityException ignored) { 202 | } 203 | for (int i = 0; i < stepCount; ++i) { 204 | y += yStep; 205 | x += xStep; 206 | eventTime = SystemClock.uptimeMillis(); 207 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); 208 | try { 209 | extSolo.getInstrumentation().sendPointerSync(event); 210 | } catch (SecurityException ignored) { 211 | } 212 | long startTime = SystemClock.uptimeMillis(); 213 | long endTime = startTime + interval; 214 | while (SystemClock.uptimeMillis() <= endTime) ; 215 | } 216 | eventTime = SystemClock.uptimeMillis(); 217 | event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, toX, toY, 0); 218 | try { 219 | extSolo.getInstrumentation().sendPointerSync(event); 220 | } catch (SecurityException ignored) { 221 | } 222 | } 223 | 224 | protected void multiDrag(float[][] points) { 225 | //landscape mode - translate position according to this. We recording as if rotation doesn't 226 | //influence on x,y clicks 227 | if (getDefaultDisplay().getOrientation() == ExtSolo.ORIENTATION_LANDSCAPE) { 228 | for (int i = 0; i < points.length; i++) { 229 | float temp = points[i][0]; 230 | points[i][0] = points[i][1]; 231 | points[i][1] = getScreenWidth() - temp; 232 | } 233 | } 234 | 235 | if (points.length > 2) { 236 | long downTime = SystemClock.uptimeMillis() + 1000; 237 | long eventTime = SystemClock.uptimeMillis(); 238 | 239 | MotionEvent event = MotionEvent.obtain(downTime, eventTime, 240 | MotionEvent.ACTION_DOWN, extSolo.toScreenX(points[0][0]), extSolo.toScreenY(points[0][1]), 0); 241 | try { 242 | extSolo.getInstrumentation().sendPointerSync(event); 243 | } catch (SecurityException ignored) { 244 | } 245 | for (int i = 1; i < points.length; i++) { 246 | eventTime = SystemClock.uptimeMillis(); 247 | event = MotionEvent.obtain(downTime, eventTime, 248 | MotionEvent.ACTION_MOVE, extSolo.toScreenX(points[i][0]), extSolo.toScreenY(points[i][1]), 0); 249 | try { 250 | extSolo.getInstrumentation().sendPointerSync(event); 251 | } catch (SecurityException ignored) { 252 | } 253 | } 254 | eventTime = SystemClock.uptimeMillis(); 255 | event = MotionEvent.obtain(downTime, eventTime, 256 | MotionEvent.ACTION_UP, 257 | extSolo.toScreenX(points[points.length - 1][0]), 258 | extSolo.toScreenY(points[points.length - 1][1]), 0); 259 | try { 260 | extSolo.getInstrumentation().sendPointerSync(event); 261 | } catch (SecurityException ignored) { 262 | } 263 | } 264 | } 265 | 266 | protected void changeDeviceLanguage(Locale locale) 267 | throws ClassNotFoundException, SecurityException, 268 | NoSuchMethodException, IllegalArgumentException, 269 | IllegalAccessException, InvocationTargetException, 270 | NoSuchFieldException { 271 | @SuppressWarnings("rawtypes") 272 | Class amnClass = Class.forName("android.app.ActivityManagerNative"); 273 | Object amn = null; 274 | Configuration config = null; 275 | 276 | // amn = ActivityManagerNative.getDefault(); 277 | Method methodGetDefault = amnClass.getMethod("getDefault"); 278 | methodGetDefault.setAccessible(true); 279 | amn = methodGetDefault.invoke(amnClass); 280 | 281 | // config = amn.getConfiguration(); 282 | Method methodGetConfiguration = amnClass.getMethod("getConfiguration"); 283 | methodGetConfiguration.setAccessible(true); 284 | config = (Configuration) methodGetConfiguration.invoke(amn); 285 | 286 | // config.userSetLocale = true; 287 | @SuppressWarnings("rawtypes") 288 | Class configClass = config.getClass(); 289 | Field f = configClass.getField("userSetLocale"); 290 | f.setBoolean(config, true); 291 | 292 | // set the locale to the new value 293 | config.locale = locale; 294 | 295 | // amn.updateConfiguration(config); 296 | Method methodUpdateConfiguration = amnClass.getMethod( 297 | "updateConfiguration", Configuration.class); 298 | methodUpdateConfiguration.setAccessible(true); 299 | methodUpdateConfiguration.invoke(amn, config); 300 | } 301 | 302 | protected void restoreLocaleIfWasChanged() { 303 | if (!locale.equals(extSolo.getInstrumentation().getTargetContext() 304 | .getResources().getConfiguration().locale)) { 305 | try { 306 | changeDeviceLanguage(locale); 307 | } catch (Exception e) { 308 | // ignored 309 | } 310 | } 311 | } 312 | 313 | private List getAllActivitiesFromApplication() throws NameNotFoundException { 314 | List result = new ArrayList(); 315 | ActivityInfo[] list = extSolo. 316 | getInstrumentation() 317 | .getContext() 318 | .getPackageManager() 319 | .getPackageInfo(extSolo.getInstrumentation().getTargetContext().getPackageName(), 320 | PackageManager.GET_ACTIVITIES).activities; 321 | for (ActivityInfo aList : list) { 322 | result.add(aList.name); 323 | } 324 | return result; 325 | } 326 | 327 | private void setUp() { 328 | try { 329 | if (mMetadataConnected) { 330 | int tries = 10; 331 | // give up to 10 seconds to bound service 332 | while (!mBound && tries-- > 0) { 333 | extSolo.soloSleep(1000); 334 | } 335 | if (isBound()) { 336 | mMetadataService.setAllActivitiesFromApplication(getAllActivitiesFromApplication()); 337 | } 338 | } 339 | } catch (RemoteException e) { 340 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_SET_ALL_ACTIVITIES); 341 | } catch (NameNotFoundException e) { 342 | Log.w(ExtSolo.TAG, e.getMessage() != null ? e.getMessage() : e.toString()); 343 | } 344 | } 345 | 346 | protected void fail(String name, Object e) { 347 | extSolo.takeScreenshot(name, true); 348 | try { 349 | if (isBound()) { 350 | mMetadataService.setErrorMessage(e.toString()); 351 | mMetadataService.addDurationToAction(); 352 | } 353 | } catch (RemoteException e1) { 354 | Log.d(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_MSG_OR_DURATION); 355 | } 356 | } 357 | 358 | protected void setGPSMockLocation(final double latitude, final double longitude, final double altitude) { 359 | if (locationManager != null) { 360 | //set timer which will regularly set desired location 361 | mockGPSTimer.schedule(new TimerTask() { 362 | @Override 363 | public void run() { 364 | Location location = new Location(LocationManager.GPS_PROVIDER); 365 | location.setLatitude(latitude); 366 | location.setLongitude(longitude); 367 | location.setAltitude(altitude); 368 | location.setAccuracy(16F); 369 | location.setTime(System.currentTimeMillis()); 370 | location.setBearing(0F); 371 | 372 | locationManager.setTestProviderLocation(LocationManager.GPS_PROVIDER, location); 373 | } 374 | }, 0, 5000); 375 | //we need to send intent in case test is launched in cloud and monitor set own location 376 | //to avoid interfere those two gps timers 377 | Intent gpsMockMonitorIntent = new Intent("com.bitbar.testdroid.monitor.START_MOCK_GPS") 378 | .setPackage("com.bitbar.testdroid.monitor"); 379 | gpsMockMonitorIntent.putExtra("latitude", Double.toString(latitude)); 380 | gpsMockMonitorIntent.putExtra("longitude", Double.toString(longitude)); 381 | gpsMockMonitorIntent.putExtra("elevation", Double.toString(altitude)); 382 | 383 | extSolo.getInstrumentation().getContext().sendBroadcast(gpsMockMonitorIntent); 384 | } else { 385 | Log.d(ExtSolo.TAG, Messages.MOCK_LOCATION_NO_PERMISSION); 386 | } 387 | } 388 | 389 | protected void resetGPSMocking() { 390 | if (locationManager != null) { 391 | if (mockGPSTimer != null) { 392 | mockGPSTimer.cancel(); 393 | } 394 | locationManager.clearTestProviderLocation(LocationManager.GPS_PROVIDER); 395 | locationManager.clearTestProviderEnabled(LocationManager.GPS_PROVIDER); 396 | 397 | //we need to also reset our mock location in monitor to revive default settings 398 | Intent gpsMockMonitorIntent = new Intent("com.bitbar.testdroid.monitor.STOP_MOCK_GPS") 399 | .setPackage("com.bitbar.testdroid.monitor"); 400 | extSolo.getInstrumentation().getContext().sendBroadcast(gpsMockMonitorIntent); 401 | } 402 | } 403 | 404 | protected String getDescriptionFromView(View view) { 405 | if (view == null) { 406 | return "null"; 407 | } 408 | String parts[] = view.getClass().getName().split("\\."); 409 | String className = parts[parts.length - 1]; 410 | 411 | String result = null; 412 | 413 | if (view instanceof CheckBox || view instanceof RadioButton || view instanceof ToggleButton) { 414 | result = String.format("\"%s\"", ((CompoundButton) view).getText().toString()); 415 | if (isNullOrEmpty(result)) { 416 | result = getDescriptionOrId(view); 417 | } 418 | result += ((CompoundButton) view).isChecked() ? " - unchecked" : " - checked"; 419 | } else if (view instanceof Button) { 420 | result = String.format("\"%s\"", ((Button) view).getText().toString()); 421 | } else if (view instanceof TextView) { 422 | result = ((TextView) view).getText().toString(); 423 | } 424 | 425 | if (isNullOrEmpty(result)) { 426 | result = getDescriptionOrId(view); 427 | } 428 | if (result == null) { 429 | return String.format("%s", className); 430 | } else { 431 | return String.format("%s %s", className, result); 432 | } 433 | } 434 | 435 | private String getDescriptionOrId(View view) { 436 | String result = view.getContentDescription() != null ? view 437 | .getContentDescription().toString() : null; 438 | if (isNullOrEmpty(result)) { 439 | if (view.getId() != View.NO_ID) { 440 | try { 441 | result = String.format("with id: \"%s\"", view.getContext() 442 | .getResources().getResourceEntryName(view.getId())); 443 | } catch (NotFoundException e) { 444 | result = String.format(Locale.ENGLISH, "with id: \"%d\"", 445 | view.getId()); 446 | } 447 | } 448 | } else { 449 | result = String.format("\"%s\"", result); 450 | } 451 | return result; 452 | } 453 | 454 | protected void turnWifi(boolean enabled) { 455 | try { 456 | WifiManager wifiManager = (WifiManager) extSolo.getInstrumentation().getTargetContext() 457 | .getSystemService(Context.WIFI_SERVICE); 458 | wifiManager.setWifiEnabled(enabled); 459 | 460 | // let know monitoring about programmatic turning on/off wifi 461 | Intent intent = new Intent(); 462 | intent.setAction("com.bitbar.testdroid.monitor.TURN_WIFI"); 463 | intent.setPackage("com.bitbar.testdroid.monitor"); 464 | intent.putExtra("enabled", enabled); 465 | extSolo.getInstrumentation().getTargetContext().sendBroadcast(intent); 466 | } catch (Exception ignored) { 467 | // don't interrupt test execution, if there 468 | // is no permission for that action 469 | } 470 | } 471 | 472 | protected boolean waitForWifi(WIFI_STATE state, int time) { 473 | if (state != null) { 474 | WifiManager wifiManager = (WifiManager) extSolo.getInstrumentation().getTargetContext() 475 | .getSystemService(Context.WIFI_SERVICE); 476 | WIFI_STATE current; 477 | long timeOut = System.currentTimeMillis() + time; 478 | while (System.currentTimeMillis() <= timeOut) { 479 | current = wifiManager.isWifiEnabled() ? WIFI_STATE.CONNECTED : WIFI_STATE.DISCONNECTED; 480 | if (state.equals(current)) { 481 | return true; 482 | } 483 | extSolo.soloSleep(INTERVAL_FOR_CHECK); 484 | } 485 | } 486 | return false; 487 | } 488 | 489 | public void addAction(String name, Type type) { 490 | Log.d(ExtSolo.TAG, name); 491 | String currentActivity = extSolo.getCurrentActivity().getClass().getCanonicalName(); 492 | try { 493 | if (isBound()) { 494 | mMetadataService.addAction(name, type.toString(), currentActivity, className, methodName); 495 | } 496 | } catch (RemoteException e) { 497 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_ADD_ACTION); 498 | } 499 | } 500 | 501 | public void addScreenshotToMetadata(String name, boolean failed) { 502 | Activity currentActivity = extSolo.getCurrentActivity(); 503 | int orientation = -1; 504 | if (currentActivity != null) { 505 | orientation = ((WindowManager) currentActivity.getSystemService(Context.WINDOW_SERVICE)) 506 | .getDefaultDisplay().getOrientation(); 507 | } 508 | try { 509 | if (isBound()) { 510 | mMetadataService.addScreenshotToMetadata(name, failed, orientation); 511 | } 512 | } catch (RemoteException e) { 513 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_ADD_SCREENSHOT); 514 | } 515 | } 516 | 517 | public void addDurationToAction() { 518 | try { 519 | if (isBound()) { 520 | mMetadataService.addDurationToAction(); 521 | } 522 | } catch (RemoteException e) { 523 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_ADD_DURATION); 524 | } 525 | } 526 | 527 | public void saveMetadataFile() { 528 | try { 529 | if (isBound()) { 530 | mMetadataService.saveMetadataFile(); 531 | } 532 | } catch (RemoteException e) { 533 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_SAVE); 534 | } catch (NullPointerException e) { 535 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_SAVE); 536 | } 537 | } 538 | 539 | public void changeActionDescription(String name) { 540 | try { 541 | if (isBound()) { 542 | mMetadataService.changeActionDescription(name); 543 | } 544 | } catch (RemoteException e) { 545 | Log.w(ExtSolo.TAG, Messages.METADATA_SERVICE_ERROR_CHANGE); 546 | } 547 | } 548 | 549 | public void tearDown() { 550 | // unbind service 551 | if (isBound()) { 552 | extSolo.getInstrumentation().getTargetContext().unbindService(mConnection); 553 | } 554 | } 555 | 556 | protected void moveGalleryLeft(final Gallery gallery, final int numberOfTimes) { 557 | for (int i = 0; i < numberOfTimes; i++) { 558 | extSolo.getInstrumentation().runOnMainSync(new Runnable() { 559 | public void run() { 560 | gallery.onKeyDown(KeyEvent.KEYCODE_DPAD_LEFT, new KeyEvent( 561 | 0, 0)); 562 | } 563 | 564 | }); 565 | extSolo.soloSleep(150); 566 | } 567 | } 568 | 569 | protected void moveGalleryRight(final Gallery gallery, final int numberOfTimes) { 570 | for (int i = 0; i < numberOfTimes; i++) { 571 | extSolo.getInstrumentation().runOnMainSync(new Runnable() { 572 | public void run() { 573 | gallery.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, new KeyEvent(0, 0)); 574 | } 575 | 576 | }); 577 | extSolo.soloSleep(150); 578 | } 579 | } 580 | 581 | private boolean isBound() { 582 | return (mMetadataService != null) && mBound; 583 | } 584 | 585 | protected void sendStringSync(String text) { 586 | extSolo.getInstrumentation().sendStringSync(text); 587 | } 588 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/ProxyWebChromeClient.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | import android.graphics.Bitmap; 6 | import android.os.Message; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.webkit.ConsoleMessage; 10 | import android.webkit.GeolocationPermissions; 11 | import android.webkit.JsPromptResult; 12 | import android.webkit.JsResult; 13 | import android.webkit.ValueCallback; 14 | import android.webkit.WebChromeClient; 15 | import android.webkit.WebStorage; 16 | import android.webkit.WebView; 17 | 18 | public class ProxyWebChromeClient extends WebChromeClient { 19 | 20 | private WebChromeClient originalWebChromeClient; 21 | private final HtmlUtils htmlUtils; 22 | 23 | public ProxyWebChromeClient(WebView webView, HtmlUtils htmlUtils) { 24 | this.htmlUtils = htmlUtils; 25 | try { 26 | Object finalWebView = webView; 27 | if (android.os.Build.VERSION.SDK_INT >= 16) { 28 | Field mProvider = WebView.class.getDeclaredField("mProvider"); 29 | mProvider.setAccessible(true); 30 | finalWebView = mProvider.get(webView); //in API >= 16 webView contain provider: WebViewClassic 31 | } 32 | 33 | Field mCallbackProxyField = finalWebView.getClass().getDeclaredField("mCallbackProxy"); 34 | mCallbackProxyField.setAccessible(true); 35 | Object mCallbackProxy = mCallbackProxyField.get(finalWebView); 36 | 37 | Field mWebChromeClientField = mCallbackProxy.getClass().getDeclaredField("mWebChromeClient"); 38 | mWebChromeClientField.setAccessible(true); 39 | originalWebChromeClient = (WebChromeClient)mWebChromeClientField.get(mCallbackProxy); 40 | 41 | Log.i(ExtSolo.TAG, (String.format(Messages.ORIGINAL_WEB_CHROME_CLIENT, originalWebChromeClient))); 42 | } catch (Exception e) { 43 | Log.w(Messages.CANNOT_GET_ORIGINAL_WEB_CHROME_CLIENT, e); 44 | } 45 | } 46 | 47 | @Override 48 | public Bitmap getDefaultVideoPoster() { 49 | if (originalWebChromeClient != null) { 50 | return originalWebChromeClient.getDefaultVideoPoster(); 51 | } else { 52 | return super.getDefaultVideoPoster(); 53 | } 54 | } 55 | 56 | @Override 57 | public View getVideoLoadingProgressView() { 58 | if (originalWebChromeClient != null) { 59 | return originalWebChromeClient.getVideoLoadingProgressView(); 60 | } else { 61 | return super.getVideoLoadingProgressView(); 62 | } 63 | } 64 | 65 | @Override 66 | public void getVisitedHistory(ValueCallback callback) { 67 | if (originalWebChromeClient != null) { 68 | originalWebChromeClient.getVisitedHistory(callback); 69 | } else { 70 | super.getVisitedHistory(callback); 71 | } 72 | } 73 | 74 | @Override 75 | public void onCloseWindow(WebView window) { 76 | if (originalWebChromeClient != null) { 77 | originalWebChromeClient.onCloseWindow(window); 78 | } else { 79 | super.onCloseWindow(window); 80 | } 81 | } 82 | 83 | @Override 84 | public void onConsoleMessage(String message, int lineNumber, String sourceID) { 85 | if (originalWebChromeClient != null) { 86 | originalWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); 87 | } else { 88 | super.onConsoleMessage(message, lineNumber, sourceID); 89 | } 90 | } 91 | 92 | @Override 93 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 94 | if (consoleMessage.message().startsWith(HtmlUtils.JS_RESULT_PREFIX)) { 95 | synchronized(htmlUtils) { 96 | htmlUtils.setJSResult(consoleMessage.message().replaceFirst(HtmlUtils.JS_RESULT_PREFIX, "")); 97 | htmlUtils.notify(); 98 | } 99 | } 100 | 101 | if (originalWebChromeClient != null) { 102 | return originalWebChromeClient.onConsoleMessage(consoleMessage); 103 | } else { 104 | return super.onConsoleMessage(consoleMessage); 105 | } 106 | } 107 | 108 | @Override 109 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { 110 | if (originalWebChromeClient != null) { 111 | return originalWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 112 | } else { 113 | return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg); 114 | } 115 | } 116 | 117 | @Override 118 | public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, 119 | long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) { 120 | if (originalWebChromeClient != null) { 121 | originalWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 122 | } else { 123 | super.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); 124 | } 125 | } 126 | 127 | @Override 128 | public void onGeolocationPermissionsHidePrompt() { 129 | if (originalWebChromeClient != null) { 130 | originalWebChromeClient.onGeolocationPermissionsHidePrompt(); 131 | } else { 132 | super.onGeolocationPermissionsHidePrompt(); 133 | } 134 | } 135 | 136 | @Override 137 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 138 | if (originalWebChromeClient != null) { 139 | originalWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); 140 | } else { 141 | super.onGeolocationPermissionsShowPrompt(origin, callback); 142 | } 143 | } 144 | 145 | @Override 146 | public void onHideCustomView() { 147 | if (originalWebChromeClient != null) { 148 | originalWebChromeClient.onHideCustomView(); 149 | } else { 150 | super.onHideCustomView(); 151 | } 152 | } 153 | 154 | @Override 155 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 156 | if (originalWebChromeClient != null) { 157 | return originalWebChromeClient.onJsAlert(view, url, message, result); 158 | } else { 159 | return super.onJsAlert(view, url, message, result); 160 | } 161 | } 162 | 163 | @Override 164 | public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) { 165 | if (originalWebChromeClient.onJsBeforeUnload(view, url, message, result)) { 166 | return originalWebChromeClient.onJsBeforeUnload(view, url, message, result); 167 | } else { 168 | return super.onJsBeforeUnload(view, url, message, result); 169 | } 170 | } 171 | 172 | @Override 173 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 174 | if (originalWebChromeClient != null) { 175 | return originalWebChromeClient.onJsConfirm(view, url, message, result); 176 | } else { 177 | return super.onJsConfirm(view, url, message, result); 178 | } 179 | } 180 | 181 | @Override 182 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 183 | if (originalWebChromeClient != null) { 184 | return originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, result); 185 | } else { 186 | return super.onJsPrompt(view, url, message, defaultValue, result); 187 | } 188 | } 189 | 190 | @Override 191 | public boolean onJsTimeout() { 192 | if (originalWebChromeClient != null) { 193 | return originalWebChromeClient.onJsTimeout(); 194 | } else { 195 | return super.onJsTimeout(); 196 | } 197 | } 198 | 199 | @Override 200 | public void onProgressChanged(WebView view, int newProgress) { 201 | if (originalWebChromeClient != null) { 202 | originalWebChromeClient.onProgressChanged(view, newProgress); 203 | } else { 204 | super.onProgressChanged(view, newProgress); 205 | } 206 | } 207 | 208 | @Override 209 | public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) { 210 | if (originalWebChromeClient != null) { 211 | originalWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 212 | } else { 213 | super.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); 214 | } 215 | } 216 | 217 | @Override 218 | public void onReceivedIcon(WebView view, Bitmap icon) { 219 | if (originalWebChromeClient != null) { 220 | originalWebChromeClient.onReceivedIcon(view, icon); 221 | } else { 222 | super.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 | } else { 231 | super.onReceivedTitle(view, title); 232 | } 233 | } 234 | 235 | @Override 236 | public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) { 237 | if (originalWebChromeClient != null) { 238 | originalWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); 239 | } else { 240 | super.onReceivedTouchIconUrl(view, url, precomposed); 241 | } 242 | } 243 | 244 | @Override 245 | public void onRequestFocus(WebView view) { 246 | if (originalWebChromeClient != null) { 247 | originalWebChromeClient.onRequestFocus(view); 248 | } else { 249 | super.onRequestFocus(view); 250 | } 251 | } 252 | 253 | @Override 254 | public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { 255 | if (originalWebChromeClient != null) { 256 | originalWebChromeClient.onShowCustomView(view, callback); 257 | } else { 258 | super.onShowCustomView(view, callback); 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/ScreenshotUtils.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | 7 | import com.bitbar.testdroid.aidl.IScreenshotService; 8 | 9 | import android.content.ComponentName; 10 | import android.content.Context; 11 | import android.content.Intent; 12 | import android.content.ServiceConnection; 13 | import android.graphics.Bitmap; 14 | import android.os.Environment; 15 | import android.os.IBinder; 16 | import android.os.RemoteException; 17 | import android.util.Log; 18 | import android.view.View; 19 | 20 | class ScreenshotUtils { 21 | // timeout for waiting for screenshot taking and sent 22 | private static final int SCREENSHOT_TIMEOUT = 20000; 23 | 24 | private ExtSolo extSolo; 25 | private boolean mBound; 26 | IScreenshotService mScreenshotService; 27 | 28 | private ServiceConnection mConnection = new ServiceConnection() { 29 | public void onServiceDisconnected(ComponentName name) { 30 | mBound = false; 31 | mScreenshotService = null; 32 | } 33 | 34 | public void onServiceConnected(ComponentName name, IBinder service) { 35 | mScreenshotService = IScreenshotService.Stub.asInterface(service); 36 | mBound = true; 37 | } 38 | }; 39 | 40 | ScreenshotUtils(ExtSolo extSolo) { 41 | this.extSolo = extSolo; 42 | extSolo.getInstrumentation().getTargetContext().bindService( 43 | new Intent("com.bitbar.testdroid.monitor.ScreenshotService").setPackage("com.bitbar.testdroid.monitor"), 44 | mConnection, Context.BIND_AUTO_CREATE); 45 | } 46 | 47 | public void tearDown() { 48 | //unbind service 49 | if (isBound()) { 50 | extSolo.getInstrumentation().getTargetContext().unbindService(mConnection); 51 | } 52 | } 53 | 54 | protected void takeScreenshot(final String name) { 55 | new Thread(new Runnable() { 56 | public void run() { 57 | boolean aslFailed = false; 58 | if (isBound()) { 59 | Log.d(ExtSolo.TAG, Messages.CONNECTED_TO_SCREENSHOT_SERVICE); 60 | try { 61 | if (!mScreenshotService.takeScreenshot(name)) { 62 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_SERVICE_FAILED); 63 | aslFailed = true; 64 | } else { 65 | unlockThread(); 66 | } 67 | } catch (RemoteException e) { 68 | Log.d(ExtSolo.TAG, Messages.COULDNT_CALL_SCREENSHOT_SERVICE_METHOD); 69 | aslFailed = true; 70 | } 71 | } else { 72 | aslFailed = true; 73 | } 74 | if (aslFailed) { 75 | if (extSolo.waitForView(View.class, 1, SCREENSHOT_TIMEOUT)) { 76 | final View view = extSolo.getViews().get(0).getRootView(); 77 | extSolo.getCurrentActivity().runOnUiThread(new Runnable() { 78 | public void run() { 79 | takeScreenshotWithoutAsl(name, view); 80 | } 81 | }); 82 | } else { 83 | Log.d(ExtSolo.TAG, Messages.SCRRENSHOT_NO_VIEWS); 84 | } 85 | } 86 | } 87 | }).start(); 88 | 89 | lockThread(); 90 | } 91 | 92 | private void unlockThread() { 93 | synchronized (ScreenshotUtils.this) { 94 | ScreenshotUtils.this.notify(); 95 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_JOB_NOTIFIED); 96 | } 97 | } 98 | 99 | private void lockThread() { 100 | // wait until screenshot is fully taken and sent 101 | synchronized (this) { 102 | try { 103 | this.wait(SCREENSHOT_TIMEOUT); 104 | } catch (InterruptedException e) { 105 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_WAIT_INTERRUPTED); 106 | } 107 | } 108 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_JOB_WOKE_UP); 109 | } 110 | 111 | protected void takeScreenshotWithoutAsl(String name, View view) { 112 | view.setDrawingCacheEnabled(true); 113 | view.buildDrawingCache(); 114 | 115 | // get drawing cache from given view 116 | Bitmap bmp = view.getDrawingCache(); 117 | 118 | if (bmp != null) { 119 | // get path for sdcard 120 | String path = String.format("%s/%s/", 121 | Environment.getExternalStorageDirectory(), 122 | "test-screenshots"); 123 | 124 | // create dir for screenshot if it doesn't exist 125 | File dir = new File(path); 126 | if (!dir.exists()) { 127 | dir.mkdirs(); 128 | } 129 | 130 | //save bitmap from drawing cache on sdcard 131 | FileOutputStream fos = null; 132 | try { 133 | fos = new FileOutputStream(String.format( 134 | "%s%s.png", path, name)); 135 | bmp.compress(Bitmap.CompressFormat.PNG, 90, fos); 136 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_SAVED_WITH_OLD_METHOD); 137 | } catch (IOException e) { 138 | Log.d(ExtSolo.TAG, 139 | String.format(Messages.SCREENSHOT_NO_PERMISSION, name)); 140 | } finally { 141 | view.destroyDrawingCache(); 142 | 143 | if (fos != null) { 144 | try { 145 | fos.close(); 146 | } catch (IOException e) { 147 | Log.d(ExtSolo.TAG, Messages.UNABLE_TO_CLOSE_OPENED_STREAM); 148 | } 149 | } 150 | 151 | unlockThread(); 152 | } 153 | } else { 154 | Log.d(ExtSolo.TAG, Messages.SCREENSHOT_DRAWING_CACHE_NULL); 155 | } 156 | } 157 | 158 | private boolean isBound() { 159 | return (mScreenshotService != null) && mBound; 160 | } 161 | } -------------------------------------------------------------------------------- /src/main/java/com/bitbar/recorder/extensions/Waiter.java: -------------------------------------------------------------------------------- 1 | package com.bitbar.recorder.extensions; 2 | 3 | import android.os.SystemClock; 4 | import android.view.View; 5 | import com.bitbar.recorder.extensions.OtherUtils.Type; 6 | 7 | class Waiter { 8 | private ExtSolo extSolo; 9 | 10 | Waiter(ExtSolo extSolo) { 11 | this.extSolo = extSolo; 12 | } 13 | 14 | public boolean wait(Class clazz, Integer index, String text, Integer timeout) { 15 | boolean result; 16 | StringBuilder sb = new StringBuilder(); 17 | sb.append("Wait for "); 18 | 19 | String[] parts = clazz.getName().split("\\."); 20 | 21 | sb.append(parts[parts.length - 1]); 22 | 23 | if (text != null) { 24 | sb.append(" with text: ").append(text); 25 | } else if (index != null) { 26 | sb.append(" with index: ").append(index); 27 | } 28 | extSolo.getOtherUtils().addAction(sb.toString(), Type.wait); 29 | if (text != null) { 30 | result = extSolo.soloWaitForText(text, timeout); 31 | } else { 32 | result = waitForView(clazz, index, timeout); 33 | } 34 | extSolo.getOtherUtils().addDurationToAction(); 35 | return result; 36 | } 37 | 38 | public boolean wait(Class clazz, Integer index, Integer timeout) { 39 | return wait(clazz, index, null, timeout); 40 | } 41 | 42 | public boolean wait(Class clazz, String text, Integer timeout) { 43 | return wait(clazz, null, text, timeout); 44 | } 45 | 46 | public boolean waitById(Class clazz, String id, int timeout) { 47 | 48 | StringBuilder sb = new StringBuilder(); 49 | sb.append("Wait for "); 50 | 51 | String[] parts = clazz.getName().split("\\."); 52 | 53 | sb.append(parts[parts.length - 1]); 54 | sb.append(" with id: ").append(id); 55 | 56 | extSolo.getOtherUtils().addAction(sb.toString(), Type.wait); 57 | boolean result = false; 58 | long endTime = SystemClock.uptimeMillis() + timeout; 59 | do { 60 | View view = extSolo.getOtherUtils().findViewById(id); 61 | if (view != null) { 62 | result = extSolo.waitForView(view); 63 | } 64 | } while (SystemClock.uptimeMillis() <= endTime && !result); 65 | extSolo.getOtherUtils().addDurationToAction(); 66 | return result; 67 | } 68 | 69 | private boolean waitForView(Class clazz, Integer index, Integer timeout) { 70 | return extSolo.waitForView(clazz, index + 1, timeout); 71 | } 72 | } --------------------------------------------------------------------------------