calabash-android 0.5.1 → 0.5.2.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/bin/calabash-android-console.rb +24 -9
  4. data/bin/calabash-android-run.rb +4 -1
  5. data/bin/calabash-android-setup.rb +1 -1
  6. data/lib/calabash-android/deprecated_actions.map +8 -0
  7. data/lib/calabash-android/env.rb +30 -2
  8. data/lib/calabash-android/environment_helpers.rb +12 -0
  9. data/lib/calabash-android/gestures.rb +308 -0
  10. data/lib/calabash-android/helpers.rb +55 -6
  11. data/lib/calabash-android/java_keystore.rb +2 -1
  12. data/lib/calabash-android/lib/TestServer.apk +0 -0
  13. data/lib/calabash-android/operations.rb +901 -707
  14. data/lib/calabash-android/removed_actions.txt +4 -0
  15. data/lib/calabash-android/steps/date_picker_steps.rb +4 -4
  16. data/lib/calabash-android/steps/time_picker_steps.rb +3 -3
  17. data/lib/calabash-android/touch_helpers.rb +114 -18
  18. data/lib/calabash-android/version.rb +1 -1
  19. data/test-server/calabash-js/src/calabash.js +42 -1
  20. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/HttpServer.java +67 -2
  21. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/MultiTouchGesture.java +749 -0
  22. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/softkey/PressKey.java +85 -0
  23. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/HideSoftKeyboard.java +24 -2
  24. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/PressUserActionButton.java +128 -0
  25. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/webview/QueryHelper.java +8 -1
  26. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/query/InvocationOperation.java +56 -9
  27. metadata +10 -8
  28. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByContentDescription.java +0 -33
  29. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByIndex.java +0 -24
  30. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByContentDescription.java +0 -34
  31. data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByIndex.java +0 -26
@@ -50,3 +50,7 @@ wait_for_tab
50
50
  set_text
51
51
  scroll_down
52
52
  scroll_up
53
+ set_date_with_description
54
+ set_date_with_index
55
+ set_time_with_description
56
+ set_time_with_index
@@ -1,8 +1,8 @@
1
1
 
2
- Given /^I set the date to "(\d\d-\d\d-\d\d\d\d)" on DatePicker with index "([^\"]*)"$/ do |date, index|
3
- perform_action('set_date_with_index', date, index)
2
+ Given /^I set the date to "(\d\d-\d\d-\d\d\d\d)" on DatePicker with index ([^\"]*)$/ do |date, index|
3
+ set_date("android.widget.DatePicker index:#{index.to_i-1}", date)
4
4
  end
5
5
 
6
6
  Given /^I set the "([^\"]*)" date to "(\d\d-\d\d-\d\d\d\d)"$/ do |content_description, date|
7
- perform_action('set_date_with_description', content_description, date)
8
- end
7
+ set_date("android.widget.DatePicker {contentDescription LIKE[c] '#{content_description}'}", date)
8
+ end
@@ -1,8 +1,8 @@
1
1
 
2
- Given /^I set the time to "(\d\d:\d\d)" on TimePicker with index "([^\"]*)"$/ do |time, index|
3
- perform_action('set_time_with_index', time, index)
2
+ Given /^I set the time to "(\d\d:\d\d)" on TimePicker with index ([^\"]*)$/ do |time, index|
3
+ set_time("android.widget.TimePicker index:#{index.to_i-1}", time)
4
4
  end
5
5
 
6
6
  Given /^I set the "([^\"]*)" time to "(\d\d:\d\d)"$/ do |content_description, time|
7
- perform_action('set_time_with_description', content_description, time)
7
+ set_time("android.widget.TimePicker {contentDescription LIKE[c] '#{content_description}'}", time)
8
8
  end
@@ -1,34 +1,120 @@
1
1
  module Calabash
2
2
  module Android
3
3
  module TouchHelpers
4
+ include ::Calabash::Android::Gestures
5
+
6
+ def execute_gesture(multi_touch_gesture)
7
+ result = JSON.parse(http("/gesture", JSON.parse(multi_touch_gesture.to_json), read_timeout: multi_touch_gesture.timeout+10))
8
+
9
+ if result['outcome'] != 'SUCCESS'
10
+ raise "Failed to perform gesture. #{result['reason']}"
11
+ end
12
+ end
13
+
4
14
  def tap(mark, *args)
15
+ puts "Warning: The method tap is deprecated. Use tap_mark instead. In later Calabash versions we will change the semantics of `tap` to take a general query."
16
+
17
+ tap_mark(mark, *args)
18
+ end
19
+
20
+ def tap_mark(mark, *args)
5
21
  touch("* marked:'#{mark}'", *args)
6
22
  end
7
23
 
8
- def double_tap(uiquery, options = {})
9
- center_x, center_y = find_coordinate(uiquery, options)
24
+ def touch(query_string, options={})
25
+ if query_result?(query_string)
26
+ center_x, center_y = find_coordinate(query_string, options)
27
+
28
+ perform_action("touch_coordinate", center_x, center_y)
29
+ else
30
+ execute_gesture(Gesture.with_parameters(Gesture.tap(options), {query_string: query_string}.merge(options)))
31
+ end
32
+ end
33
+
34
+ def double_tap(query_string, options={})
35
+ if query_result?(query_string)
36
+ center_x, center_y = find_coordinate(query_string, options)
10
37
 
11
- perform_action("double_tap_coordinate", center_x, center_y)
38
+ perform_action("double_tap_coordinate", center_x, center_y)
39
+ else
40
+ execute_gesture(Gesture.with_parameters(Gesture.double_tap(options), {query_string: query_string}.merge(options)))
41
+ end
12
42
  end
13
43
 
14
- # Performs a "long press" operation on a selected view
15
- # Params:
16
- # +uiquery+: a uiquery identifying one view
17
- # +options[:length]+: the length of the long press in milliseconds (optional)
18
- #
19
- # Examples:
20
- # - long_press("* id:'my_id'")
21
- # - long_press("* id:'my_id'", {:length=>5000})
22
- def long_press(uiquery, options = {})
23
- center_x, center_y = find_coordinate(uiquery, options)
24
- length = options[:length]
25
- perform_action("long_press_coordinate", center_x, center_y, *(length unless length.nil?))
44
+ def long_press(query_string, options={})
45
+ if query_result?(query_string)
46
+ center_x, center_y = find_coordinate(query_string, options)
47
+ length = options[:length]
48
+
49
+ perform_action("long_press_coordinate", center_x, center_y, *(length unless length.nil?))
50
+ else
51
+ length = options[:length]
52
+
53
+ if length
54
+ puts "Using the length key is deprecated. Use 'time' (in seconds) instead."
55
+ options[:time] = length/1000.0
56
+ end
57
+
58
+ options[:time] ||= 1
59
+
60
+ touch(query_string, options)
61
+ end
62
+ end
63
+
64
+ def drag(*args)
65
+ pan(*args)
26
66
  end
27
67
 
28
- def touch(uiquery, options = {})
29
- center_x, center_y = find_coordinate(uiquery, options)
68
+ def pan_left(options={})
69
+ pan("DecorView", :left, options)
70
+ end
71
+
72
+ def pan_right(options={})
73
+ pan("DecorView", :right, options)
74
+ end
30
75
 
31
- perform_action("touch_coordinate", center_x, center_y)
76
+ def pan_up(options={})
77
+ pan("* id:'content'", :up, options)
78
+ end
79
+
80
+ def pan_down(options={})
81
+ pan("* id:'content'", :down, options)
82
+ end
83
+
84
+ def pan(query_string, direction, options={})
85
+ execute_gesture(Gesture.with_parameters(Gesture.swipe(direction, options), {query_string: query_string}.merge(options)))
86
+ end
87
+
88
+ def flick_left(options={})
89
+ flick("DecorView", :left, options)
90
+ end
91
+
92
+ def flick_right(options={})
93
+ flick("DecorView", :right, options)
94
+ end
95
+
96
+ def flick_up(options={})
97
+ flick("* id:'content'", :up, options)
98
+ end
99
+
100
+ def flick_down(options={})
101
+ flick("* id:'content'", :down, options)
102
+ end
103
+
104
+ def flick(query_string, direction, options={})
105
+ execute_gesture(Gesture.with_parameters(Gesture.swipe(direction, {flick: true}.merge(options)), {query_string: query_string}.merge(options)))
106
+ end
107
+
108
+ def pinch_out(options={})
109
+ pinch("* id:'content'", :out, options)
110
+ end
111
+
112
+ def pinch_in(options={})
113
+ pinch("* id:'content'", :in, options)
114
+ end
115
+
116
+ def pinch(query_string, direction, options={})
117
+ execute_gesture(Gesture.with_parameters(Gesture.pinch(direction, options), {query_string: query_string}.merge(options)))
32
118
  end
33
119
 
34
120
  def find_coordinate(uiquery, options={})
@@ -68,6 +154,16 @@ module Calabash
68
154
  when_element_exists(query_string, options)
69
155
  end
70
156
  end
157
+
158
+ def query_result?(uiquery)
159
+ element = if uiquery.is_a?(Array)
160
+ uiquery.first
161
+ else
162
+ uiquery
163
+ end
164
+
165
+ element.is_a?(Hash) && element.has_key?('rect') && element['rect'].has_key?('center_x') && element['rect'].has_key?('center_y')
166
+ end
71
167
  end
72
168
  end
73
169
  end
@@ -1,5 +1,5 @@
1
1
  module Calabash
2
2
  module Android
3
- VERSION = "0.5.1"
3
+ VERSION = "0.5.2.pre1"
4
4
  end
5
5
  end
@@ -98,8 +98,40 @@
98
98
  return res;
99
99
  }
100
100
 
101
+ function applyMethods(object, arguments) {
102
+ var length = arguments.length;
103
+
104
+ for(var i = 0; i < length; i++) {
105
+ var argument = arguments[i];
106
+
107
+ if (typeof argument === 'string') {
108
+ argument = {method_name: argument, arguments: []}
109
+ }
110
+
111
+ var methodName = argument.method_name;
112
+ var methodArguments = argument.arguments;
113
+
114
+ if (typeof object[methodName] === 'undefined') {
115
+ var type = Object.prototype.toString.call(object);
116
+
117
+ object =
118
+ {
119
+ error: "No such method '" + methodName + "'",
120
+ methodName: methodName,
121
+ receiverString: object.constructor.name,
122
+ receiverClass: type
123
+ };
124
+
125
+ break;
126
+ } else {
127
+ object = object[methodName].apply(object, methodArguments);
128
+ }
129
+ }
130
+ }
131
+
101
132
  var exp = '%@'/* dynamic */,
102
- queryType = '%@',
133
+ queryType = '%@' /* dynamic */,
134
+ arguments = '%@' /* dynamic */,
103
135
  nodes = null,
104
136
  res = [],
105
137
  i,N;
@@ -122,5 +154,14 @@
122
154
  {
123
155
  return JSON.stringify({error:'Exception while running query: '+exp, details:e.toString()})
124
156
  }
157
+
158
+ if (arguments !== '%@') {
159
+ var length = res.length;
160
+
161
+ for (var i = 0; i < length; i++) {
162
+ res[i] = applyMethods(res[i], arguments);
163
+ }
164
+ }
165
+
125
166
  return JSON.stringify(toJSON(res));
126
167
  })();
@@ -9,6 +9,7 @@ import java.io.StringWriter;
9
9
  import java.lang.InterruptedException;
10
10
  import java.lang.Override;
11
11
  import java.lang.Runnable;
12
+ import java.util.Collections;
12
13
  import java.util.Enumeration;
13
14
  import java.util.List;
14
15
  import java.util.Map;
@@ -23,10 +24,14 @@ import sh.calaba.instrumentationbackend.FranklyResult;
23
24
  import sh.calaba.instrumentationbackend.InstrumentationBackend;
24
25
  import sh.calaba.instrumentationbackend.Result;
25
26
  import sh.calaba.instrumentationbackend.json.JSONUtils;
27
+ import sh.calaba.instrumentationbackend.query.InvocationOperation;
28
+ import sh.calaba.instrumentationbackend.query.Operation;
26
29
  import sh.calaba.instrumentationbackend.query.Query;
27
30
  import sh.calaba.instrumentationbackend.query.QueryResult;
28
31
  import sh.calaba.org.codehaus.jackson.map.ObjectMapper;
29
32
 
33
+ import android.app.Application;
34
+ import android.content.Context;
30
35
  import android.graphics.Bitmap;
31
36
  import android.util.Log;
32
37
  import android.view.View;
@@ -123,7 +128,48 @@ public class HttpServer extends NanoHTTPD {
123
128
  errorResult = FranklyResult.fromThrowable(e);
124
129
  }
125
130
  return new NanoHTTPD.Response(HTTP_INTERNALERROR, "application/json;charset=utf-8", errorResult.asJson());
126
- }
131
+ }
132
+ else if (uri.endsWith("/backdoor")) {
133
+ try {
134
+ String json = params.getProperty("json");
135
+ ObjectMapper mapper = new ObjectMapper();
136
+ Map backdoorMethod = mapper.readValue(json, Map.class);
137
+
138
+ String methodName = (String) backdoorMethod.get("method_name");
139
+ List arguments = (List) backdoorMethod.get("arguments");
140
+ Operation operation = new InvocationOperation(methodName, arguments);
141
+
142
+ Application application = InstrumentationBackend.solo.getCurrentActivity().getApplication();
143
+ Object invocationResult;
144
+
145
+ invocationResult = operation.apply(application);
146
+
147
+ if (invocationResult instanceof Map && ((Map) invocationResult).containsKey("error")) {
148
+ Context context = getRootView().getContext();
149
+ invocationResult = operation.apply(context);
150
+ }
151
+
152
+ Map<String, String> result = new HashMap<String, String>();
153
+
154
+ if (invocationResult instanceof Map && ((Map) invocationResult).containsKey("error")) {
155
+ result.put("outcome", "ERROR");
156
+ result.put("result", (String) ((Map) invocationResult).get("error"));
157
+ result.put("details", invocationResult.toString());
158
+ } else {
159
+ result.put("outcome", "SUCCESS");
160
+ result.put("result", String.valueOf(invocationResult));
161
+ }
162
+
163
+ ObjectMapper resultMapper = new ObjectMapper();
164
+
165
+ return new NanoHTTPD.Response(HTTP_OK, "application/json;charset=utf-8", resultMapper.writeValueAsString(result));
166
+ } catch (Exception e) {
167
+ e.printStackTrace();
168
+ Exception ex = new Exception("Could not invoke method", e);
169
+
170
+ return new NanoHTTPD.Response(HTTP_OK, "application/json;charset=utf-8", FranklyResult.fromThrowable(ex).asJson());
171
+ }
172
+ }
127
173
  else if (uri.endsWith("/map")) {
128
174
  FranklyResult errorResult = null;
129
175
  try {
@@ -189,7 +235,26 @@ public class HttpServer extends NanoHTTPD {
189
235
  } else if (uri.endsWith("/query")) {
190
236
  return new Response(HTTP_BADREQUEST, MIME_PLAINTEXT,
191
237
  "/query endpoint is discontinued - use /map with operation query");
192
- } else if (uri.endsWith("/kill")) {
238
+ } else if (uri.endsWith("/gesture")) {
239
+ FranklyResult errorResult;
240
+
241
+ try {
242
+ String json = params.getProperty("json");
243
+ ObjectMapper mapper = new ObjectMapper();
244
+ Map gesture = mapper.readValue(json, Map.class);
245
+
246
+ (new MultiTouchGesture(gesture)).perform();
247
+
248
+ return new NanoHTTPD.Response(HTTP_OK, "application/json;charset=utf-8",
249
+ FranklyResult.successResult(new QueryResult(Collections.emptyList())).asJson());
250
+
251
+ } catch (Exception e ) {
252
+ e.printStackTrace();
253
+ errorResult = FranklyResult.fromThrowable(e);
254
+ }
255
+
256
+ return new NanoHTTPD.Response(HTTP_OK, "application/json;charset=utf-8", errorResult.asJson());
257
+ } else if (uri.endsWith("/kill")) {
193
258
  lock.lock();
194
259
  try {
195
260
  running = false;
@@ -0,0 +1,749 @@
1
+ package sh.calaba.instrumentationbackend.actions;
2
+
3
+ import android.app.Activity;
4
+ import android.app.Instrumentation;
5
+ import android.app.KeyguardManager;
6
+ import android.content.Context;
7
+ import android.os.Build;
8
+ import android.os.SystemClock;
9
+ import android.util.Pair;
10
+ import android.view.MotionEvent;
11
+
12
+ import java.lang.reflect.Method;
13
+
14
+ import java.util.ArrayList;
15
+ import java.util.HashMap;
16
+ import java.util.List;
17
+ import java.util.Map;
18
+
19
+ import sh.calaba.instrumentationbackend.InstrumentationBackend;
20
+ import sh.calaba.instrumentationbackend.query.Query;
21
+ import sh.calaba.instrumentationbackend.query.QueryResult;
22
+
23
+ public class MultiTouchGesture {
24
+ Map<String, Object> multiTouchGestureMap;
25
+ Instrumentation instrumentation;
26
+ List<Gesture> pressedGestures;
27
+ List<Gesture> gesturesToPerform;
28
+ boolean hasPressedFirstGesture;
29
+
30
+ public MultiTouchGesture(Map<String, Object> multiTouchGesture) {
31
+ this.multiTouchGestureMap = multiTouchGesture;
32
+ instrumentation = InstrumentationBackend.instrumentation;
33
+ gesturesToPerform = new ArrayList<Gesture>();
34
+ hasPressedFirstGesture = false;
35
+ }
36
+
37
+ public void parseGesture() {
38
+ List<Map<String, Object>> sentGestures = (ArrayList<Map<String, Object>>) multiTouchGestureMap.get("gestures");
39
+ List<String> queryStrings = new ArrayList<String>();
40
+
41
+ // We query before generating the gestures. TODO: Implement new class with temporary information
42
+ for (Map<String, Object> gestureMap : sentGestures) {
43
+ String queryString = (String) gestureMap.get("query_string");
44
+
45
+ if (queryString != null) {
46
+ queryStrings.add(queryString);
47
+ }
48
+
49
+ ArrayList<Map<String, Object>> sentTouches = (ArrayList<Map<String, Object>>) gestureMap.get("touches");
50
+
51
+ for (Map<String,Object> sentTouch : sentTouches) {
52
+ if (sentTouch.containsKey("query_string")) {
53
+ queryString = (String) sentTouch.get("query_string");
54
+
55
+ if (queryString != null) {
56
+ queryStrings.add(queryString);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ long timeout = (long) ((Double) multiTouchGestureMap.get("query_timeout") * 1000);
63
+ Map<String, Map<String, Integer>> evaluatedQueries = evaluateQueries(queryStrings, timeout);
64
+
65
+ for (Map<String, Object> gestureMap : sentGestures) {
66
+ String queryString = (String) gestureMap.get("query_string");
67
+ Map<String,Integer> rect = evaluatedQueries.get(queryString);
68
+
69
+ ArrayList<Map<String, Object>> sentTouches = (ArrayList<Map<String, Object>>) gestureMap.get("touches");
70
+
71
+ Gesture gesture = new Gesture();
72
+ int length = sentTouches.size();
73
+
74
+ for (int i = 0; i < length; i++) {
75
+ Map<String, Object> touch = sentTouches.get(i);
76
+ Map<String,Integer> specificRect = rect;
77
+
78
+ if (touch.containsKey("query_string")) {
79
+ String specificQueryString = (String) touch.get("query_string");
80
+
81
+ if (specificQueryString != null) {
82
+ specificRect = evaluatedQueries.get(specificQueryString);
83
+ }
84
+ }
85
+
86
+ int resultX = 0, resultY = 0, resultWidth = 0, resultHeight = 0;
87
+
88
+ if (specificRect != null) {
89
+ resultX = specificRect.get("x");
90
+ resultY = specificRect.get("y");
91
+ resultWidth = specificRect.get("width");
92
+ resultHeight = specificRect.get("height");
93
+ }
94
+
95
+ int offsetX = (Integer) touch.get("offset_x");
96
+ int offsetY = (Integer) touch.get("offset_y");
97
+ int x, y;
98
+
99
+ if (specificRect == null) {
100
+ x = offsetX;
101
+ y = offsetY;
102
+ } else {
103
+ x = ((((Integer) touch.get("x")) * resultWidth)/100 + offsetX + resultX);
104
+ y = ((((Integer) touch.get("y")) * resultHeight)/100 + offsetY + resultY);
105
+ }
106
+
107
+ long time = (long) ((Double) touch.get("time") * 1000);
108
+ long wait = (long) ((Double) touch.get("wait") * 1000);
109
+ boolean release = (Boolean) touch.get("release");
110
+
111
+ if (i == length - 1) {
112
+ release = true;
113
+ }
114
+
115
+ if (!gesture.hasEvents()) {
116
+ gesture.addEvent(new Event(new Coordinate(x, y), wait));
117
+ } else {
118
+ gesture.addEvent(new Event(new Coordinate(x, y), 0l));
119
+
120
+ if (wait != 0l) {
121
+ gesture.addEvent(new Event(new Coordinate(x, y), wait), false);
122
+ } else {
123
+ gesture.setFling(true);
124
+ }
125
+ }
126
+
127
+ gesture.addTimeOffset(time);
128
+
129
+ if (release) {
130
+ gesture.addEvent(new Event(gesture.upEvent().getCoordinate(), 0l), true);
131
+ long endTime = gesture.upEvent().getTime();
132
+ gesturesToPerform.add(gesture);
133
+ gesture = new Gesture(endTime);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ public void perform() {
140
+ parseGesture();
141
+
142
+ // Sometimes the keyguard window or a service pops up very briefly.
143
+ // We handle it by waiting a fixed time
144
+ tryWaitForKeyguard(2);
145
+
146
+ long time;
147
+ long startTime = SystemClock.uptimeMillis();
148
+ long endTime = findEndTime();
149
+ pressedGestures = new ArrayList<Gesture>();
150
+
151
+ while ((time = SystemClock.uptimeMillis() - startTime) <= endTime) {
152
+ releaseGestures(time);
153
+
154
+ if (hasPressedFirstGesture) {
155
+ pressGestures(time);
156
+
157
+ // Instead of reassigning the time variable we only move gestures when we no longer offset the time
158
+ moveGestures(time);
159
+ } else {
160
+ // We catch the security exception thrown when the keyguard is visible briefly and retry if it's the first press
161
+ // Offset the startTime by the needed extra time if needed
162
+ long currentTime = SystemClock.uptimeMillis();
163
+ pressGestures(time);
164
+ long timeTaken = SystemClock.uptimeMillis() - currentTime;
165
+
166
+ // Offset the start time (this changing the future values of time). This is done to keep relative timings consistent
167
+ if (hasPressedFirstGesture) { // We just pressed our first gesture
168
+ startTime += timeTaken;
169
+ }
170
+ }
171
+ }
172
+
173
+ releaseGestures(endTime);
174
+ }
175
+
176
+ private Map<String, Map<String, Integer>> evaluateQueries(List<String> queryStrings, long timeout) {
177
+ List<String> distinctQueryStrings = new ArrayList<String>();
178
+
179
+ for (String queryString : queryStrings) {
180
+ if (!distinctQueryStrings.contains(queryString)) {
181
+ distinctQueryStrings.add(queryString);
182
+ }
183
+ }
184
+
185
+ Map<String, Map<String, Integer>> evaluatedQueries = new HashMap<String, Map<String, Integer>>();
186
+
187
+ long endTime = SystemClock.uptimeMillis() + timeout;
188
+
189
+ do {
190
+ evaluatedQueries.clear();
191
+
192
+ for (String queryString : distinctQueryStrings) {
193
+ QueryResult queryResult = new Query(queryString, java.util.Collections.emptyList()).executeQuery();
194
+ // For now we calculate the views location and save it. In an implementation in the future, we should save the actual views and use their coordinates later on.
195
+ List<Object> results = queryResult.asList();
196
+
197
+ if (results.size() == 0) {
198
+ break;
199
+ } else {
200
+ Map<Object,Object> firstItem = (Map<Object, Object>) results.get(0);
201
+ Map<String,Integer> rect = (Map<String, Integer>) firstItem.get("rect");
202
+ evaluatedQueries.put(queryString, rect);
203
+ }
204
+ }
205
+
206
+ if (evaluatedQueries.size() == distinctQueryStrings.size()) {
207
+ // All the queries have been evaluated
208
+ return evaluatedQueries;
209
+ }
210
+
211
+ // Avoid affecting the UI Thread and device performance too much
212
+ try {
213
+ Thread.sleep(500);
214
+ } catch (InterruptedException e) {
215
+ throw new RuntimeException(e);
216
+ }
217
+ } while (SystemClock.uptimeMillis() <= endTime);
218
+
219
+ throw new RuntimeException("Could not find views '" + distinctQueryStrings.toString() + "'");
220
+ }
221
+
222
+ private void sendPointerSync(MotionEvent motionEvent) {
223
+ instrumentation.sendPointerSync(motionEvent);
224
+ }
225
+
226
+ private void pressGestures(long currentTime) {
227
+ int i = 0;
228
+
229
+ while (i < gesturesToPerform.size()) {
230
+ Gesture gesture = gesturesToPerform.get(i);
231
+
232
+ // Sometimes the keyguard window or a service pops up very briefly.
233
+ // We handle it by retrying - but only if no gestures are currently down to avoid mistiming gestures.
234
+ int retries = hasPressedFirstGesture ? 1 : 10;
235
+ SecurityException ex = null;
236
+
237
+ for (int j = 0; j < retries; j++) {
238
+ try {
239
+ if (gesture.shouldPress(currentTime)) {
240
+ sendPointerSync(gesture.generateDownEvent(pressedGestures));
241
+ hasPressedFirstGesture = true;
242
+ pressedGestures.add(gesture);
243
+ gesturesToPerform.remove(i);
244
+ } else {
245
+ i++;
246
+ }
247
+
248
+ ex = null;
249
+ break;
250
+ } catch (SecurityException e) {
251
+ ex = e;
252
+
253
+ try {
254
+ Thread.sleep(100);
255
+ } catch (InterruptedException interruptedException) {
256
+ throw new RuntimeException(interruptedException);
257
+ }
258
+ }
259
+ }
260
+
261
+ if (ex != null) {
262
+ throw new SecurityException(ex);
263
+ }
264
+ }
265
+ }
266
+
267
+ private void releaseGestures(long currentTime) {
268
+ int i = 0;
269
+
270
+ while (i < pressedGestures.size()) {
271
+ Gesture gesture = pressedGestures.get(i);
272
+
273
+ if (gesture.shouldRelease(currentTime)) {
274
+ // Ensure that the gesture will always move to its end-coordinate
275
+ long uptimeMillis = SystemClock.uptimeMillis();
276
+ moveGestures(currentTime, uptimeMillis);
277
+
278
+ sendPointerSync(gesture.generateUpEvent(pressedGestures, uptimeMillis+1));
279
+ pressedGestures.remove(i);
280
+ } else {
281
+ i++;
282
+ }
283
+ }
284
+ }
285
+
286
+ private void moveGestures(long currentTime) {
287
+ moveGestures(currentTime, null);
288
+ }
289
+
290
+ private void moveGestures(long currentTime, Long systemClockUptimeMillis) {
291
+ List<Gesture> gesturesToMove = new ArrayList<Gesture>();
292
+
293
+ for (Gesture gesture : pressedGestures) {
294
+ // Flinging gestures should never move to their last coordinate before releasing the touch.
295
+ if (gesture.shouldMove(currentTime)) {
296
+ gesturesToMove.add(gesture);
297
+ }
298
+ }
299
+
300
+ int gestureLength = gesturesToMove.size();
301
+
302
+ if (gestureLength > 0) {
303
+ MotionEvent motionEvent = obtainMotionEvent(gesturesToMove, MotionEvent.ACTION_MOVE, currentTime, findAbsoluteStartTime(), systemClockUptimeMillis);
304
+ sendPointerSync(motionEvent);
305
+ }
306
+ }
307
+
308
+ private long findAbsoluteStartTime() {
309
+ long startTime = SystemClock.uptimeMillis();
310
+
311
+ for (Gesture gesture : pressedGestures) {
312
+ if (gesture.getAbsoluteDownTime() != null) {
313
+ startTime = Math.min(gesture.getAbsoluteDownTime(), startTime);
314
+ }
315
+ }
316
+
317
+ return startTime;
318
+ }
319
+
320
+ private long findEndTime() {
321
+ long endTime = 0;
322
+
323
+ for (Gesture gesture : gesturesToPerform) {
324
+ endTime = Math.max(gesture.upEvent().getTime(), endTime);
325
+ }
326
+
327
+ return endTime;
328
+ }
329
+
330
+ public static void tryWaitForKeyguard(int timeoutSeconds) {
331
+ Activity activity = InstrumentationBackend.solo.getCurrentActivity();
332
+ KeyguardManager keyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
333
+
334
+ if (!keyguardManager.inKeyguardRestrictedInputMode()) {
335
+ return;
336
+ }
337
+
338
+ long startTime = SystemClock.uptimeMillis();
339
+
340
+ while (SystemClock.uptimeMillis() - startTime <= timeoutSeconds * 1000) {
341
+ if (!keyguardManager.inKeyguardRestrictedInputMode()) {
342
+ break;
343
+ }
344
+ }
345
+
346
+ // For now, if the keyguard has shown up once, we sleep a bit more.
347
+ // TODO: Improve the implementation to detect focus of the activity to replace stopping when the keyguard is gone
348
+
349
+ try {
350
+ Thread.sleep(100);
351
+ } catch (InterruptedException e) {
352
+ e.printStackTrace();
353
+ }
354
+ }
355
+
356
+ public static MotionEvent obtainMotionEvent(List<Gesture> pressedGestures, int motionEventAction, long currentTime, Long absoluteDownTime) {
357
+ return obtainMotionEvent(pressedGestures, motionEventAction, currentTime, absoluteDownTime, null);
358
+ }
359
+
360
+ public static MotionEvent obtainMotionEvent(List<Gesture> pressedGestures, int motionEventAction, long currentTime, Long absoluteDownTime, Long systemClockUptimeMillis) {
361
+ int gestureLength = pressedGestures.size();
362
+
363
+ Coordinate[] coordinates = new Coordinate[gestureLength];
364
+ int[] pointerIds = new int[gestureLength];
365
+
366
+ for (int i = 0; i < gestureLength; i++) {
367
+ Gesture gesture = pressedGestures.get(i);
368
+ coordinates[i] = gesture.getPosition(currentTime);
369
+ pointerIds[i] = gesture.getId();
370
+ }
371
+
372
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) {
373
+ MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[gestureLength];
374
+
375
+ for (int i = 0; i < gestureLength; i++) {
376
+ MotionEvent.PointerCoords pointerCoord = new MotionEvent.PointerCoords();
377
+ pointerCoord.x = coordinates[i].getX();
378
+ pointerCoord.y = coordinates[i].getY();
379
+ pointerCoord.pressure = 1;
380
+ pointerCoord.size = 1;
381
+ pointerCoords[i] = pointerCoord;
382
+ }
383
+
384
+ if (systemClockUptimeMillis == null) {
385
+ systemClockUptimeMillis = SystemClock.uptimeMillis();
386
+ }
387
+
388
+ if (absoluteDownTime == null) {
389
+ absoluteDownTime = systemClockUptimeMillis;
390
+ }
391
+
392
+ return MotionEvent.obtain(absoluteDownTime, systemClockUptimeMillis, motionEventAction, gestureLength, pointerIds, pointerCoords, 0, 1, 1, 0, 0, 0, 0);
393
+ } else {
394
+ try {
395
+ Method method = MotionEvent.class.getMethod("obtainNano", long.class, long.class, long.class, int.class, int.class, int[].class, float[].class, int.class, float.class, float.class, int.class, int.class);
396
+ float[] inData = new float[4*(gestureLength)];
397
+
398
+ for (int i = 0; i < gestureLength; i++) {
399
+ inData[i*4] = coordinates[i].getX();
400
+ inData[i*4+1] = coordinates[i].getY();
401
+ inData[i*4+2] = 1.0f;
402
+ inData[i*4+3] = 1.0f;
403
+ }
404
+
405
+ if (systemClockUptimeMillis == null) {
406
+ systemClockUptimeMillis = SystemClock.uptimeMillis();
407
+ }
408
+
409
+ if (absoluteDownTime == null) {
410
+ absoluteDownTime = systemClockUptimeMillis;
411
+ }
412
+
413
+ return (MotionEvent)method.invoke(null, absoluteDownTime, systemClockUptimeMillis, systemClockUptimeMillis * 1000000, motionEventAction, gestureLength, pointerIds, inData, 0, 1, 1, 0, 0);
414
+ } catch (Exception e) {
415
+ e.printStackTrace();
416
+ throw new RuntimeException(e);
417
+ }
418
+ }
419
+ }
420
+
421
+ private class Gesture {
422
+ List<Event> events;
423
+ Integer id;
424
+ Long absoluteDownTime;
425
+ long timeOffset;
426
+ boolean fling;
427
+
428
+ public Gesture() {
429
+ this(0);
430
+ }
431
+
432
+ public Gesture(long timeOffset) {
433
+ events = new ArrayList<Event>();
434
+ id = null;
435
+ absoluteDownTime = null;
436
+ this.timeOffset = timeOffset;
437
+ setFling(false);
438
+ }
439
+
440
+ public void addTimeOffset(long timeOffset) {
441
+ this.timeOffset += timeOffset;
442
+ }
443
+
444
+ public long getTimeOffset() {
445
+ return timeOffset;
446
+ }
447
+
448
+ public Integer getId() {
449
+ return id;
450
+ }
451
+
452
+ public boolean getFling() {
453
+ return fling;
454
+ }
455
+
456
+ public Long getAbsoluteDownTime() {
457
+ return absoluteDownTime;
458
+ }
459
+
460
+ public void setFling(boolean fling) {
461
+ this.fling = fling;
462
+ }
463
+
464
+ public void addEvent(Event event) {
465
+ addEvent(event, true);
466
+ }
467
+
468
+ public void addEvent(Event event, boolean useOffset) {
469
+ if (upEvent() != null) {
470
+ event.setTime(event.getTime() + upEvent().getTime());
471
+ }
472
+
473
+ if (useOffset) {
474
+ event.setTime(event.getTime() + timeOffset);
475
+ }
476
+
477
+ events.add(event);
478
+ timeOffset = 0;
479
+ }
480
+
481
+ public boolean hasEvents() {
482
+ return (events != null && events.size() != 0);
483
+ }
484
+
485
+ public Event downEvent() {
486
+ if (!hasEvents()) {
487
+ return null;
488
+ } else {
489
+ return events.get(0);
490
+ }
491
+ }
492
+
493
+ public Event upEvent() {
494
+ if (!hasEvents()) {
495
+ return null;
496
+ } else {
497
+ return events.get(events.size() - 1);
498
+ }
499
+ }
500
+
501
+ public Pair<Event,Event> getEventPair(long time) {
502
+ if (downEvent().getTime() == time) {
503
+ return new Pair<Event,Event>(downEvent(), events.get(1));
504
+ }
505
+
506
+ int i;
507
+
508
+ for (i = 0; i < events.size(); i++) {
509
+ if (events.get(i).getTime() > time) {
510
+ break;
511
+ }
512
+ }
513
+
514
+ if (i == 0) {
515
+ return null;
516
+ }
517
+
518
+ return new Pair<Event,Event>(events.get(i-1), events.get(i));
519
+ }
520
+
521
+ public boolean shouldPress(long time) {
522
+ return (getEventPair(time) != null);
523
+ }
524
+
525
+ public boolean shouldRelease(long time) {
526
+ return (time >= upEvent().getTime());
527
+ }
528
+
529
+ public boolean shouldMove(long time) {
530
+ if (!getFling() || time >= upEvent().getTime()) {
531
+ return true;
532
+ }
533
+
534
+ Coordinate position = getPosition(time);
535
+ Coordinate upCoordinate = upEvent().getCoordinate();
536
+ double fullDistance = getEventPair(time).first.getCoordinate().distance(upCoordinate);
537
+ double distance = position.distance(upCoordinate);
538
+
539
+ return (distance / fullDistance > 0.05);
540
+ }
541
+
542
+ public Coordinate getPosition(long time) {
543
+ if (time >= upEvent().getTime()) {
544
+ return upEvent().getCoordinate();
545
+ }
546
+
547
+ Pair<Event,Event> eventPair = getEventPair(time);
548
+
549
+ long startTime = eventPair.first.getTime();
550
+ long endTime = eventPair.second.getTime();
551
+ Coordinate from = eventPair.first.getCoordinate();
552
+ Coordinate to = eventPair.second.getCoordinate();
553
+
554
+ if (endTime == startTime) {
555
+ return to;
556
+ }
557
+
558
+ float fraction = (float) (time - startTime)/(endTime - startTime);
559
+
560
+ if (fraction == 0.0f) {
561
+ return from;
562
+ } else {
563
+ return from.between(to, fraction);
564
+ }
565
+ }
566
+
567
+ public MotionEvent.PointerProperties getPointerProperty() {
568
+ MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
569
+ pointerProperties.id = id;
570
+ pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
571
+
572
+ return pointerProperties;
573
+ }
574
+
575
+ public MotionEvent.PointerCoords getPointerCoord(long time) {
576
+ Coordinate position = getPosition(time);
577
+ MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
578
+ pointerCoords.x = position.getX();
579
+ pointerCoords.y = position.getY();
580
+ pointerCoords.pressure = 1.0f;
581
+ pointerCoords.size = 1.0f;
582
+
583
+ return pointerCoords;
584
+ }
585
+
586
+ public void setId(List<Gesture> pressedGestures) {
587
+ // The id of the pointer is 0-based. The id should always be as low as possible
588
+ int id = 0;
589
+ boolean idSet = false;
590
+
591
+ while (!idSet) {
592
+ idSet = true;
593
+
594
+ for (Gesture gesture : pressedGestures) {
595
+ if (gesture.getId() == id) {
596
+ idSet = false;
597
+ id++;
598
+ }
599
+ }
600
+ }
601
+
602
+ this.id = id;
603
+ }
604
+
605
+ public MotionEvent generateDownEvent(List<Gesture> pressedGestures) {
606
+ int gestureLength = pressedGestures.size();
607
+ setId(pressedGestures);
608
+ int motionEventAction;
609
+
610
+ // The down action is based on the amount of gestures currently down, not its own id/timing.
611
+ if (gestureLength > 0) {
612
+ motionEventAction = (gestureLength << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + MotionEvent.ACTION_POINTER_DOWN;
613
+ } else {
614
+ motionEventAction = MotionEvent.ACTION_DOWN;
615
+ }
616
+
617
+ long time = downEvent().getTime();
618
+
619
+ // Because this gesture is not yet down, it is not included in the list of gestures passed as the first parameter
620
+ List<Gesture> allPressedGestures = new ArrayList<Gesture>(pressedGestures);
621
+ allPressedGestures.add(this);
622
+
623
+ absoluteDownTime = SystemClock.uptimeMillis();
624
+
625
+ return obtainMotionEvent(allPressedGestures, motionEventAction, time, absoluteDownTime);
626
+ }
627
+
628
+ public MotionEvent generateUpEvent(List<Gesture> pressedGestures) {
629
+ return generateUpEvent(pressedGestures, null);
630
+ }
631
+
632
+ public MotionEvent generateUpEvent(List<Gesture> pressedGestures, Long systemClockUptimeMillis) {
633
+ int gestureLength = pressedGestures.size();
634
+ int motionEventAction;
635
+ int indexInPressedGestures = pressedGestures.indexOf(this);
636
+
637
+ // The up action is based on the amount of gestures currently down, not its own id/timing.
638
+ if (gestureLength > 1) {
639
+ motionEventAction = (indexInPressedGestures << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + MotionEvent.ACTION_POINTER_UP;
640
+ } else {
641
+ motionEventAction = MotionEvent.ACTION_UP;
642
+ }
643
+
644
+ long time = upEvent().getTime();
645
+
646
+ return obtainMotionEvent(pressedGestures, motionEventAction, time, getAbsoluteDownTime(), systemClockUptimeMillis);
647
+ }
648
+
649
+ public String toString() {
650
+ return "Gesture {Events: "+events.toString()+"}";
651
+ }
652
+ }
653
+
654
+ private class Event {
655
+ private Coordinate coordinate;
656
+ private Long time;
657
+
658
+ public Event() {
659
+ this(null, null);
660
+ }
661
+
662
+ public Event(Coordinate coordinate, Long time) {
663
+ setCoordinate(coordinate);
664
+ setTime(time);
665
+ }
666
+
667
+ public Event copy() {
668
+ return new Event(coordinate.copy(), getTime());
669
+ }
670
+
671
+ public Coordinate getCoordinate() {
672
+ return coordinate;
673
+ }
674
+
675
+ public void setCoordinate(Coordinate coordinate) {
676
+ this.coordinate = coordinate;
677
+ }
678
+
679
+ public long getTime() {
680
+ return time;
681
+ }
682
+
683
+ public void setTime(Long time) {
684
+ this.time = time;
685
+ }
686
+
687
+ public String toString() {
688
+ return "Event {time: "+getTime()+", coordinate: "+getCoordinate()+"}";
689
+ }
690
+ }
691
+
692
+ private class Coordinate {
693
+ private int x;
694
+ private int y;
695
+
696
+ public Coordinate(int x, int y) {
697
+ setX(x);
698
+ setY(y);
699
+ }
700
+
701
+ public Coordinate copy() {
702
+ return new Coordinate(getX(), getY());
703
+ }
704
+
705
+ public int getX() {
706
+ return x;
707
+ }
708
+
709
+ public int getY() {
710
+ return y;
711
+ }
712
+
713
+ public void setX(int x) {
714
+ this.x = x;
715
+ }
716
+
717
+ public void setY(int y) {
718
+ this.y = y;
719
+ }
720
+
721
+ public Coordinate between(Coordinate to, float fraction) {
722
+ int x = (int) ((to.getX() - getX()) * fraction + getX());
723
+ int y = (int) ((to.getY() - getY()) * fraction + getY());
724
+
725
+ return new Coordinate(x, y);
726
+ }
727
+
728
+ public double distance(Coordinate to) {
729
+ float xDistance = getX() - to.getX();
730
+ float yDistance = getY() - to.getY();
731
+
732
+ return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
733
+ }
734
+
735
+ public String toString() {
736
+ return "Coordinate {x: "+getX()+", y: "+getY()+"}";
737
+ }
738
+
739
+ public boolean equals(Object object, double precision) {
740
+ if (!(object instanceof Coordinate)) {
741
+ return false;
742
+ }
743
+
744
+ Coordinate coordinate = (Coordinate)object;
745
+
746
+ return (distance(coordinate) <= precision);
747
+ }
748
+ }
749
+ }