├── .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 | }
--------------------------------------------------------------------------------