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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/bin/calabash-android-console.rb +24 -9
- data/bin/calabash-android-run.rb +4 -1
- data/bin/calabash-android-setup.rb +1 -1
- data/lib/calabash-android/deprecated_actions.map +8 -0
- data/lib/calabash-android/env.rb +30 -2
- data/lib/calabash-android/environment_helpers.rb +12 -0
- data/lib/calabash-android/gestures.rb +308 -0
- data/lib/calabash-android/helpers.rb +55 -6
- data/lib/calabash-android/java_keystore.rb +2 -1
- data/lib/calabash-android/lib/TestServer.apk +0 -0
- data/lib/calabash-android/operations.rb +901 -707
- data/lib/calabash-android/removed_actions.txt +4 -0
- data/lib/calabash-android/steps/date_picker_steps.rb +4 -4
- data/lib/calabash-android/steps/time_picker_steps.rb +3 -3
- data/lib/calabash-android/touch_helpers.rb +114 -18
- data/lib/calabash-android/version.rb +1 -1
- data/test-server/calabash-js/src/calabash.js +42 -1
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/HttpServer.java +67 -2
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/MultiTouchGesture.java +749 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/softkey/PressKey.java +85 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/HideSoftKeyboard.java +24 -2
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/text/PressUserActionButton.java +128 -0
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/webview/QueryHelper.java +8 -1
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/query/InvocationOperation.java +56 -9
- metadata +10 -8
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByContentDescription.java +0 -33
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetDateByIndex.java +0 -24
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByContentDescription.java +0 -34
- data/test-server/instrumentation-backend/src/sh/calaba/instrumentationbackend/actions/time/SetTimeByIndex.java +0 -26
@@ -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
|
3
|
-
|
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
|
-
|
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
|
3
|
-
|
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
|
-
|
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
|
9
|
-
|
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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
29
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
+
}
|