calabash-android 0.5.1 → 0.5.2.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }