@rk911/tracker-sdk 1.7.0 → 1.7.2
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.
|
@@ -2,48 +2,121 @@ package com.renrui.tracker;
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
4
|
import android.app.Application;
|
|
5
|
+
import android.content.ContentValues;
|
|
6
|
+
import android.content.Context;
|
|
7
|
+
import android.database.Cursor;
|
|
8
|
+
import android.database.sqlite.SQLiteDatabase;
|
|
9
|
+
import android.database.sqlite.SQLiteOpenHelper;
|
|
5
10
|
import android.os.Bundle;
|
|
6
11
|
import android.os.Handler;
|
|
7
12
|
import android.os.HandlerThread;
|
|
8
13
|
import android.text.TextUtils;
|
|
9
14
|
import android.util.Log;
|
|
15
|
+
import android.view.ActionMode;
|
|
16
|
+
import android.view.KeyEvent;
|
|
17
|
+
import android.view.Menu;
|
|
18
|
+
import android.view.MenuItem;
|
|
19
|
+
import android.view.MotionEvent;
|
|
20
|
+
import android.view.SearchEvent;
|
|
21
|
+
import android.view.View;
|
|
22
|
+
import android.view.ViewConfiguration;
|
|
23
|
+
import android.view.ViewGroup;
|
|
24
|
+
import android.view.Window;
|
|
25
|
+
import android.view.WindowManager;
|
|
26
|
+
import android.widget.Button;
|
|
27
|
+
import android.widget.EditText;
|
|
28
|
+
import android.widget.TextView;
|
|
29
|
+
|
|
30
|
+
import androidx.annotation.NonNull;
|
|
31
|
+
import androidx.annotation.Nullable;
|
|
32
|
+
import androidx.fragment.app.Fragment;
|
|
33
|
+
import androidx.fragment.app.FragmentActivity;
|
|
34
|
+
import androidx.fragment.app.FragmentManager;
|
|
10
35
|
|
|
11
36
|
import org.json.JSONArray;
|
|
12
37
|
import org.json.JSONObject;
|
|
13
38
|
|
|
39
|
+
import java.io.BufferedReader;
|
|
40
|
+
import java.io.InputStream;
|
|
41
|
+
import java.io.InputStreamReader;
|
|
14
42
|
import java.io.OutputStream;
|
|
15
43
|
import java.net.HttpURLConnection;
|
|
16
44
|
import java.net.URL;
|
|
17
45
|
import java.util.ArrayList;
|
|
18
46
|
import java.util.List;
|
|
19
|
-
import java.util.UUID;
|
|
20
47
|
|
|
21
48
|
/**
|
|
22
49
|
* 行业级 Native Android Tracker SDK
|
|
23
|
-
* 独立后台 HandlerThread 处理打点,主线程耗时 0ms
|
|
50
|
+
* 1. 独立后台 HandlerThread 处理打点,主线程耗时 0ms
|
|
51
|
+
* 2. 轻量自包含 SQLite 本地持久化队列,应用退后台/被杀/Crash 100% 防丢 (S1)
|
|
52
|
+
* 3. 弱网/断网失败重试与补偿机制 (S2)
|
|
53
|
+
* 4. TouchSlop 滑动误触过滤,彻底排除列表滚动误触发点击 (S3)
|
|
54
|
+
* 5. 页面来源 Referrer 状态机 (S4)
|
|
55
|
+
* 6. 对外暴露标准显式操控 API (S5)
|
|
56
|
+
* 7. 兼容历史自定义点位,支撑后端双写 (A6)
|
|
24
57
|
*/
|
|
25
58
|
public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
26
59
|
|
|
27
60
|
private static final String TAG = "AndroidTracker";
|
|
28
61
|
private static final int BATCH_SIZE = 20;
|
|
29
|
-
private static final long FLUSH_INTERVAL_MS =
|
|
62
|
+
private static final long FLUSH_INTERVAL_MS = 3000;
|
|
63
|
+
private static final int MAX_DB_RECORDS = 1000;
|
|
30
64
|
|
|
31
65
|
private static volatile AndroidTracker instance;
|
|
32
66
|
private Application application;
|
|
33
|
-
private String serverUrl;
|
|
67
|
+
private volatile String serverUrl;
|
|
34
68
|
private String appId = "12"; // 12: Android App
|
|
35
|
-
private String sessionId;
|
|
36
|
-
private String deviceId;
|
|
37
|
-
private String userId;
|
|
38
|
-
private String phone;
|
|
39
|
-
private String storeName;
|
|
69
|
+
private volatile String sessionId;
|
|
70
|
+
private volatile String deviceId;
|
|
71
|
+
private volatile String userId;
|
|
72
|
+
private volatile String phone;
|
|
73
|
+
private volatile String storeName;
|
|
40
74
|
private int sequence = 0;
|
|
41
75
|
|
|
42
76
|
private HandlerThread bgThread;
|
|
43
77
|
private Handler bgHandler;
|
|
44
|
-
private
|
|
78
|
+
private TrackerDbHelper dbHelper;
|
|
79
|
+
private final Object dbLock = new Object();
|
|
80
|
+
|
|
45
81
|
private long lastPageStartTime = 0;
|
|
46
82
|
private String currentPageRoute = "";
|
|
83
|
+
private String referrerPageRoute = "";
|
|
84
|
+
private String lastClickKey = "";
|
|
85
|
+
private long lastClickTime = 0;
|
|
86
|
+
|
|
87
|
+
private static class EventRecord {
|
|
88
|
+
final long id;
|
|
89
|
+
final JSONObject json;
|
|
90
|
+
|
|
91
|
+
EventRecord(long id, JSONObject json) {
|
|
92
|
+
this.id = id;
|
|
93
|
+
this.json = json;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static class TrackerDbHelper extends SQLiteOpenHelper {
|
|
98
|
+
private static final String DB_NAME = "rk_tracker.db";
|
|
99
|
+
private static final int DB_VERSION = 1;
|
|
100
|
+
private static final String TABLE_EVENTS = "events";
|
|
101
|
+
|
|
102
|
+
TrackerDbHelper(Context context) {
|
|
103
|
+
super(context, DB_NAME, null, DB_VERSION);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Override
|
|
107
|
+
public void onCreate(SQLiteDatabase db) {
|
|
108
|
+
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_EVENTS + " ("
|
|
109
|
+
+ "id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
110
|
+
+ "data TEXT NOT NULL, "
|
|
111
|
+
+ "created_at INTEGER NOT NULL)");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Override
|
|
115
|
+
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
116
|
+
db.execSQL("DROP TABLE IF EXISTS " + TABLE_EVENTS);
|
|
117
|
+
onCreate(db);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
47
120
|
|
|
48
121
|
private AndroidTracker() {}
|
|
49
122
|
|
|
@@ -63,12 +136,27 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
63
136
|
*/
|
|
64
137
|
public void init(Application app, String serverUrl, String deviceId, String userId, String phone, String storeName) {
|
|
65
138
|
this.application = app;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
139
|
+
if (serverUrl != null && !serverUrl.contains("/api/point/task/trackBatch")) {
|
|
140
|
+
if (serverUrl.endsWith("/")) {
|
|
141
|
+
this.serverUrl = serverUrl + "api/point/task/trackBatch";
|
|
142
|
+
} else {
|
|
143
|
+
this.serverUrl = serverUrl + "/api/point/task/trackBatch";
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
this.serverUrl = serverUrl;
|
|
147
|
+
}
|
|
148
|
+
this.deviceId = !TextUtils.isEmpty(deviceId) ? deviceId : "dev_android_" + System.currentTimeMillis();
|
|
149
|
+
this.userId = userId != null ? userId : "";
|
|
150
|
+
this.phone = phone != null ? phone : "";
|
|
151
|
+
this.storeName = storeName != null ? storeName : "";
|
|
152
|
+
this.sessionId = "android_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 10000);
|
|
153
|
+
|
|
154
|
+
// 初始化 SQLite 本地持久化数据库 (S1 防丢保障)
|
|
155
|
+
try {
|
|
156
|
+
dbHelper = new TrackerDbHelper(app.getApplicationContext());
|
|
157
|
+
} catch (Exception e) {
|
|
158
|
+
Log.e(TAG, "===> [Native Tracker DB Init Error]", e);
|
|
159
|
+
}
|
|
72
160
|
|
|
73
161
|
// 创建独立后台线程处理队列与网络请求,保障 UI 0 卡顿
|
|
74
162
|
bgThread = new HandlerThread("TrackerBgThread");
|
|
@@ -80,19 +168,66 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
80
168
|
|
|
81
169
|
// 启动定时 Flush 任务
|
|
82
170
|
scheduleTimerFlush();
|
|
83
|
-
Log.
|
|
171
|
+
Log.i(TAG, "====================================================================");
|
|
172
|
+
Log.i(TAG, "===> [Native Tracker Init Success] SessionId: " + sessionId);
|
|
173
|
+
Log.i(TAG, "===> ServerUrl: " + this.serverUrl);
|
|
174
|
+
Log.i(TAG, "===> DeviceId: " + this.deviceId + ", UserId: " + this.userId + ", Phone: " + this.phone + ", Store: " + this.storeName);
|
|
175
|
+
Log.i(TAG, "====================================================================");
|
|
176
|
+
|
|
177
|
+
// 启动时检查是否有上一轮未发出的积压事件,立即尝试补发 (S2 补偿机制)
|
|
178
|
+
flushAsync();
|
|
84
179
|
}
|
|
85
180
|
|
|
86
181
|
public void updateUser(String userId, String phone, String storeName) {
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
182
|
+
if (userId != null) this.userId = userId;
|
|
183
|
+
if (phone != null) this.phone = phone;
|
|
184
|
+
if (storeName != null) this.storeName = storeName;
|
|
185
|
+
Log.i(TAG, "===> [Native Tracker User Updated] UserId: " + this.userId + ", Phone: " + this.phone + ", Store: " + this.storeName);
|
|
90
186
|
}
|
|
91
187
|
|
|
92
188
|
public String getSessionId() {
|
|
93
189
|
return sessionId;
|
|
94
190
|
}
|
|
95
191
|
|
|
192
|
+
public String getDeviceId() {
|
|
193
|
+
return deviceId;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public void resetSession() {
|
|
197
|
+
this.sessionId = "android_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 10000);
|
|
198
|
+
Log.i(TAG, "===> [Native Tracker Session Reset] New SessionId: " + this.sessionId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 手动页面访问打点 (S5 & A4 用于 ViewPager2 Tab 切换补报)
|
|
203
|
+
*/
|
|
204
|
+
public void trackPageView(final String pageRoute, final String pageTitle) {
|
|
205
|
+
postBgTask(new Runnable() {
|
|
206
|
+
@Override
|
|
207
|
+
public void run() {
|
|
208
|
+
try {
|
|
209
|
+
// S34 类似防御:500ms 内相同路由防抖,避免生命周期和手动调用重复触发
|
|
210
|
+
if (pageRoute.equals(currentPageRoute) && (System.currentTimeMillis() - lastPageStartTime < 500)) {
|
|
211
|
+
Log.d(TAG, "===> [Native Page View Skipped - Duplicate within 500ms]: " + pageRoute);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
String referrer = currentPageRoute;
|
|
215
|
+
currentPageRoute = pageRoute;
|
|
216
|
+
referrerPageRoute = referrer;
|
|
217
|
+
lastPageStartTime = System.currentTimeMillis();
|
|
218
|
+
|
|
219
|
+
Log.i(TAG, "===> [Native Page View] Route: " + currentPageRoute + ", Title: " + pageTitle + ", Referrer: " + referrerPageRoute);
|
|
220
|
+
JSONObject event = createBaseEvent("page_view", "page_view");
|
|
221
|
+
event.put("pageTitle", !TextUtils.isEmpty(pageTitle) ? pageTitle : pageRoute);
|
|
222
|
+
enqueueEvent(event);
|
|
223
|
+
flushAsync();
|
|
224
|
+
} catch (Exception e) {
|
|
225
|
+
Log.e(TAG, "===> [Native trackPageView Error]", e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
96
231
|
/**
|
|
97
232
|
* 手动自定义埋点 (Custom Tracking)
|
|
98
233
|
*/
|
|
@@ -106,8 +241,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
106
241
|
event.put("extraData", customProperties.toString());
|
|
107
242
|
}
|
|
108
243
|
enqueueEvent(event);
|
|
244
|
+
flushAsync(); // 自定义点位立即上报
|
|
109
245
|
} catch (Exception e) {
|
|
110
|
-
Log.e(TAG, "
|
|
246
|
+
Log.e(TAG, "===> [Native Tracker Track Failed]", e);
|
|
111
247
|
}
|
|
112
248
|
}
|
|
113
249
|
});
|
|
@@ -126,8 +262,9 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
126
262
|
event.put("elementId", elementId);
|
|
127
263
|
if (postParam != null) event.put("postParam", postParam);
|
|
128
264
|
enqueueEvent(event);
|
|
265
|
+
flushAsync();
|
|
129
266
|
} catch (Exception e) {
|
|
130
|
-
Log.e(TAG, "trackFrontUrl
|
|
267
|
+
Log.e(TAG, "===> [Native Tracker trackFrontUrl Failed]", e);
|
|
131
268
|
}
|
|
132
269
|
}
|
|
133
270
|
});
|
|
@@ -136,21 +273,48 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
136
273
|
private JSONObject createBaseEvent(String eventType, String eventName) throws Exception {
|
|
137
274
|
sequence++;
|
|
138
275
|
JSONObject event = new JSONObject();
|
|
139
|
-
event.put("eventId", "e_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000));
|
|
276
|
+
event.put("eventId", "e_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000));
|
|
140
277
|
event.put("eventType", eventType);
|
|
141
278
|
event.put("eventName", eventName);
|
|
142
279
|
event.put("timestamp", System.currentTimeMillis());
|
|
143
280
|
event.put("sequence", sequence);
|
|
144
281
|
event.put("pageRoute", currentPageRoute);
|
|
282
|
+
if (!TextUtils.isEmpty(referrerPageRoute)) {
|
|
283
|
+
event.put("referrer", referrerPageRoute);
|
|
284
|
+
}
|
|
145
285
|
event.put("os", "Android");
|
|
286
|
+
|
|
287
|
+
JSONObject extra = new JSONObject();
|
|
288
|
+
if (!TextUtils.isEmpty(phone)) extra.put("phone", phone);
|
|
289
|
+
if (!TextUtils.isEmpty(storeName)) extra.put("storeName", storeName);
|
|
290
|
+
if (!TextUtils.isEmpty(userId)) extra.put("userId", userId);
|
|
291
|
+
extra.put("platform", "android");
|
|
292
|
+
event.put("extraData", extra.toString());
|
|
146
293
|
return event;
|
|
147
294
|
}
|
|
148
295
|
|
|
296
|
+
/**
|
|
297
|
+
* S1 本地持久化入库:崩溃/被杀 100% 防丢
|
|
298
|
+
*/
|
|
149
299
|
private void enqueueEvent(JSONObject event) {
|
|
150
|
-
synchronized (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
300
|
+
synchronized (dbLock) {
|
|
301
|
+
if (dbHelper != null) {
|
|
302
|
+
try {
|
|
303
|
+
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
|
304
|
+
ContentValues cv = new ContentValues();
|
|
305
|
+
cv.put("data", event.toString());
|
|
306
|
+
cv.put("created_at", System.currentTimeMillis());
|
|
307
|
+
db.insert(TrackerDbHelper.TABLE_EVENTS, null, cv);
|
|
308
|
+
|
|
309
|
+
// 限制最大记录数防膨胀
|
|
310
|
+
db.execSQL("DELETE FROM " + TrackerDbHelper.TABLE_EVENTS + " WHERE id NOT IN ("
|
|
311
|
+
+ "SELECT id FROM " + TrackerDbHelper.TABLE_EVENTS + " ORDER BY id DESC LIMIT " + MAX_DB_RECORDS + ")");
|
|
312
|
+
|
|
313
|
+
Log.i(TAG, "===> [Native Tracker Event Persisted to SQLite] Type: " + event.optString("eventType")
|
|
314
|
+
+ ", Name: " + event.optString("eventName") + ", Route: " + event.optString("pageRoute"));
|
|
315
|
+
} catch (Exception e) {
|
|
316
|
+
Log.e(TAG, "===> [Native Tracker DB Insert Failed]", e);
|
|
317
|
+
}
|
|
154
318
|
}
|
|
155
319
|
}
|
|
156
320
|
}
|
|
@@ -170,40 +334,85 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
170
334
|
postBgTask(new Runnable() {
|
|
171
335
|
@Override
|
|
172
336
|
public void run() {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
337
|
+
flushInternal();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public void flushSync() {
|
|
343
|
+
postBgTask(new Runnable() {
|
|
344
|
+
@Override
|
|
345
|
+
public void run() {
|
|
346
|
+
flushInternal();
|
|
180
347
|
}
|
|
181
348
|
});
|
|
182
349
|
}
|
|
183
350
|
|
|
184
|
-
private void
|
|
185
|
-
|
|
351
|
+
private void flushInternal() {
|
|
352
|
+
int maxBatches = 5; // 单次最多连续发送 5 批(共 100 条),防止网络恢复时有大量积压滞留
|
|
353
|
+
while (maxBatches-- > 0) {
|
|
354
|
+
List<EventRecord> records = new ArrayList<>();
|
|
355
|
+
synchronized (dbLock) {
|
|
356
|
+
if (dbHelper != null) {
|
|
357
|
+
Cursor cursor = null;
|
|
358
|
+
try {
|
|
359
|
+
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
|
360
|
+
cursor = db.rawQuery("SELECT id, data FROM " + TrackerDbHelper.TABLE_EVENTS + " ORDER BY id ASC LIMIT " + BATCH_SIZE, null);
|
|
361
|
+
while (cursor != null && cursor.moveToNext()) {
|
|
362
|
+
long id = cursor.getLong(0);
|
|
363
|
+
String dataStr = cursor.getString(1);
|
|
364
|
+
records.add(new EventRecord(id, new JSONObject(dataStr)));
|
|
365
|
+
}
|
|
366
|
+
} catch (Exception e) {
|
|
367
|
+
Log.e(TAG, "===> [Native Tracker DB Query Failed]", e);
|
|
368
|
+
} finally {
|
|
369
|
+
if (cursor != null) cursor.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (records.isEmpty()) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
boolean sent = sendBatch(records);
|
|
379
|
+
if (!sent || records.size() < BATCH_SIZE) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* S2 网络发送与失败重试补偿机制
|
|
387
|
+
*/
|
|
388
|
+
private boolean sendBatch(List<EventRecord> records) {
|
|
389
|
+
if (TextUtils.isEmpty(serverUrl) || records.isEmpty()) return false;
|
|
186
390
|
HttpURLConnection conn = null;
|
|
391
|
+
boolean success = false;
|
|
187
392
|
try {
|
|
188
393
|
JSONObject body = new JSONObject();
|
|
189
394
|
body.put("appId", Integer.parseInt(appId));
|
|
190
395
|
body.put("sessionId", sessionId);
|
|
191
396
|
body.put("deviceId", deviceId);
|
|
192
|
-
body.put("userId", userId);
|
|
193
|
-
body.put("phone", phone);
|
|
194
|
-
body.put("storeName", storeName);
|
|
397
|
+
body.put("userId", userId != null ? userId : "");
|
|
398
|
+
body.put("phone", phone != null ? phone : "");
|
|
399
|
+
body.put("storeName", storeName != null ? storeName : "");
|
|
195
400
|
body.put("platform", "android");
|
|
196
401
|
|
|
197
402
|
JSONArray eventsArr = new JSONArray();
|
|
198
|
-
for (
|
|
199
|
-
eventsArr.put(
|
|
403
|
+
for (EventRecord record : records) {
|
|
404
|
+
eventsArr.put(record.json);
|
|
200
405
|
}
|
|
201
406
|
body.put("events", eventsArr);
|
|
202
407
|
|
|
408
|
+
Log.i(TAG, "===> [Native Tracker Flushing] Sending " + records.size() + " native events to: " + serverUrl);
|
|
409
|
+
|
|
203
410
|
URL url = new URL(serverUrl);
|
|
204
411
|
conn = (HttpURLConnection) url.openConnection();
|
|
205
412
|
conn.setRequestMethod("POST");
|
|
206
413
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
|
414
|
+
conn.setRequestProperty("appId", appId);
|
|
415
|
+
conn.setRequestProperty("deviceId", deviceId != null ? deviceId : "");
|
|
207
416
|
conn.setDoOutput(true);
|
|
208
417
|
conn.setConnectTimeout(5000);
|
|
209
418
|
conn.setReadTimeout(5000);
|
|
@@ -215,12 +424,51 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
215
424
|
os.close();
|
|
216
425
|
|
|
217
426
|
int code = conn.getResponseCode();
|
|
218
|
-
|
|
427
|
+
InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
|
428
|
+
String responseStr = "";
|
|
429
|
+
if (is != null) {
|
|
430
|
+
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
|
|
431
|
+
StringBuilder sb = new StringBuilder();
|
|
432
|
+
String line;
|
|
433
|
+
while ((line = reader.readLine()) != null) {
|
|
434
|
+
sb.append(line);
|
|
435
|
+
}
|
|
436
|
+
reader.close();
|
|
437
|
+
responseStr = sb.toString();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (code >= 200 && code < 300) {
|
|
441
|
+
success = true;
|
|
442
|
+
Log.i(TAG, "===> [Native Tracker Send SUCCESS] Code: " + code + ", Events: " + records.size() + ", Response: " + responseStr);
|
|
443
|
+
} else {
|
|
444
|
+
Log.e(TAG, "===> [Native Tracker Send FAILED] HTTP Code: " + code + ", Response: " + responseStr);
|
|
445
|
+
}
|
|
219
446
|
} catch (Exception e) {
|
|
220
|
-
Log.e(TAG, "
|
|
447
|
+
Log.e(TAG, "===> [Native Tracker Network Error] Send batch failed (will retry in next flush)", e);
|
|
221
448
|
} finally {
|
|
222
449
|
if (conn != null) conn.disconnect();
|
|
223
450
|
}
|
|
451
|
+
|
|
452
|
+
// 成功上报才从本地 SQLite 清理已上报事件;失败则保留在本地库中,等待下次补偿重试 (S2)
|
|
453
|
+
if (success) {
|
|
454
|
+
synchronized (dbLock) {
|
|
455
|
+
if (dbHelper != null) {
|
|
456
|
+
try {
|
|
457
|
+
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
|
458
|
+
StringBuilder sb = new StringBuilder("id IN (");
|
|
459
|
+
for (int i = 0; i < records.size(); i++) {
|
|
460
|
+
sb.append(records.get(i).id);
|
|
461
|
+
if (i < records.size() - 1) sb.append(",");
|
|
462
|
+
}
|
|
463
|
+
sb.append(")");
|
|
464
|
+
db.delete(TrackerDbHelper.TABLE_EVENTS, sb.toString(), null);
|
|
465
|
+
} catch (Exception e) {
|
|
466
|
+
Log.e(TAG, "===> [Native Tracker DB Delete Clean Failed]", e);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return success;
|
|
224
472
|
}
|
|
225
473
|
|
|
226
474
|
private void postBgTask(Runnable runnable) {
|
|
@@ -229,7 +477,139 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
229
477
|
}
|
|
230
478
|
}
|
|
231
479
|
|
|
232
|
-
// --- Activity Lifecycle Auto
|
|
480
|
+
// --- Activity & Fragment Lifecycle & Auto Native Click Tracking ---
|
|
481
|
+
|
|
482
|
+
@Override
|
|
483
|
+
public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
|
|
484
|
+
// 1. 自动注册 Support Fragment 生命周期回调
|
|
485
|
+
if (activity instanceof FragmentActivity) {
|
|
486
|
+
((FragmentActivity) activity).getSupportFragmentManager()
|
|
487
|
+
.registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() {
|
|
488
|
+
@Override
|
|
489
|
+
public void onFragmentResumed(FragmentManager fm, Fragment f) {
|
|
490
|
+
super.onFragmentResumed(fm, f);
|
|
491
|
+
final String fragmentRoute = f.getClass().getName();
|
|
492
|
+
postBgTask(new Runnable() {
|
|
493
|
+
@Override
|
|
494
|
+
public void run() {
|
|
495
|
+
try {
|
|
496
|
+
String referrer = currentPageRoute;
|
|
497
|
+
currentPageRoute = fragmentRoute;
|
|
498
|
+
referrerPageRoute = referrer;
|
|
499
|
+
lastPageStartTime = System.currentTimeMillis();
|
|
500
|
+
Log.i(TAG, "===> [Native Fragment Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
|
|
501
|
+
JSONObject event = createBaseEvent("page_view", "page_view");
|
|
502
|
+
event.put("pageTitle", currentPageRoute);
|
|
503
|
+
enqueueEvent(event);
|
|
504
|
+
flushAsync();
|
|
505
|
+
} catch (Exception e) {
|
|
506
|
+
Log.e(TAG, "===> [Native Fragment Resumed Error]", e);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}, true);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 2. 自动拦截 Window.Callback,捕获 Native 控件点击 (带 S3 滑动防误触过滤)
|
|
515
|
+
try {
|
|
516
|
+
Window window = activity.getWindow();
|
|
517
|
+
if (window != null) {
|
|
518
|
+
final Window.Callback origCb = window.getCallback();
|
|
519
|
+
window.setCallback(new WindowCallbackWrapper(origCb, activity));
|
|
520
|
+
}
|
|
521
|
+
} catch (Exception e) {
|
|
522
|
+
Log.w(TAG, "===> [Native Window Callback Install Failed]", e);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private boolean isClickableTarget(View view) {
|
|
527
|
+
if (view == null) return false;
|
|
528
|
+
if (view.isClickable() || view.isLongClickable()) return true;
|
|
529
|
+
if (view instanceof Button || view instanceof EditText) return true;
|
|
530
|
+
return (view instanceof TextView && !TextUtils.isEmpty(((TextView) view).getText()));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private View findTouchedLeafView(View root, int rawX, int rawY) {
|
|
534
|
+
if (root == null || root.getVisibility() != View.VISIBLE) return null;
|
|
535
|
+
|
|
536
|
+
int[] location = new int[2];
|
|
537
|
+
root.getLocationOnScreen(location);
|
|
538
|
+
int x = location[0];
|
|
539
|
+
int y = location[1];
|
|
540
|
+
int w = root.getWidth();
|
|
541
|
+
int h = root.getHeight();
|
|
542
|
+
|
|
543
|
+
if (rawX < x || rawX > x + w || rawY < y || rawY > y + h) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (root instanceof ViewGroup) {
|
|
548
|
+
ViewGroup group = (ViewGroup) root;
|
|
549
|
+
for (int i = group.getChildCount() - 1; i >= 0; i--) {
|
|
550
|
+
View child = group.getChildAt(i);
|
|
551
|
+
View target = findTouchedLeafView(child, rawX, rawY);
|
|
552
|
+
if (target != null) {
|
|
553
|
+
return target;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return root;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private void handleNativeViewClick(final Activity activity, final View target, final float rawX, final float rawY) {
|
|
562
|
+
final String elementText;
|
|
563
|
+
if (target instanceof TextView) {
|
|
564
|
+
elementText = ((TextView) target).getText().toString().trim();
|
|
565
|
+
} else {
|
|
566
|
+
elementText = "";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
final String elementId;
|
|
570
|
+
if (target.getId() != View.NO_ID) {
|
|
571
|
+
String resName = "";
|
|
572
|
+
try {
|
|
573
|
+
resName = target.getResources().getResourceEntryName(target.getId());
|
|
574
|
+
} catch (Exception e) {}
|
|
575
|
+
elementId = !TextUtils.isEmpty(resName) ? resName : "id_" + target.getId();
|
|
576
|
+
} else {
|
|
577
|
+
elementId = target.getClass().getSimpleName();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
final String key = elementId + "|" + elementText;
|
|
581
|
+
final long now = System.currentTimeMillis();
|
|
582
|
+
if (key.equals(lastClickKey) && (now - lastClickTime < 800)) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
lastClickKey = key;
|
|
586
|
+
lastClickTime = now;
|
|
587
|
+
|
|
588
|
+
postBgTask(new Runnable() {
|
|
589
|
+
@Override
|
|
590
|
+
public void run() {
|
|
591
|
+
try {
|
|
592
|
+
JSONObject event = createBaseEvent("click", "native_click");
|
|
593
|
+
event.put("elementId", elementId);
|
|
594
|
+
if (!TextUtils.isEmpty(elementText)) {
|
|
595
|
+
event.put("elementText", elementText.length() > 30 ? elementText.substring(0, 30) : elementText);
|
|
596
|
+
}
|
|
597
|
+
JSONObject extra = new JSONObject();
|
|
598
|
+
extra.put("viewType", target.getClass().getName());
|
|
599
|
+
extra.put("pageX", rawX);
|
|
600
|
+
extra.put("pageY", rawY);
|
|
601
|
+
extra.put("isNative", true);
|
|
602
|
+
event.put("extraData", extra.toString());
|
|
603
|
+
|
|
604
|
+
Log.i(TAG, "===> [Native Auto Click Captured] Id: " + elementId + ", Text: " + elementText);
|
|
605
|
+
enqueueEvent(event);
|
|
606
|
+
flushAsync();
|
|
607
|
+
} catch (Exception e) {
|
|
608
|
+
Log.e(TAG, "===> [Native Click Handle Error]", e);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
233
613
|
|
|
234
614
|
@Override
|
|
235
615
|
public void onActivityResumed(final Activity activity) {
|
|
@@ -237,13 +617,17 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
237
617
|
@Override
|
|
238
618
|
public void run() {
|
|
239
619
|
try {
|
|
620
|
+
String referrer = currentPageRoute;
|
|
240
621
|
currentPageRoute = activity.getClass().getName();
|
|
622
|
+
referrerPageRoute = referrer;
|
|
241
623
|
lastPageStartTime = System.currentTimeMillis();
|
|
624
|
+
Log.i(TAG, "===> [Native Activity Resumed] Page: " + currentPageRoute + ", Referrer: " + referrerPageRoute);
|
|
242
625
|
JSONObject event = createBaseEvent("page_view", "page_view");
|
|
243
626
|
event.put("pageTitle", activity.getTitle() != null ? activity.getTitle().toString() : currentPageRoute);
|
|
244
627
|
enqueueEvent(event);
|
|
628
|
+
flushAsync();
|
|
245
629
|
} catch (Exception e) {
|
|
246
|
-
Log.e(TAG, "onActivityResumed
|
|
630
|
+
Log.e(TAG, "===> [Native Tracker onActivityResumed Error]", e);
|
|
247
631
|
}
|
|
248
632
|
}
|
|
249
633
|
});
|
|
@@ -261,17 +645,100 @@ public class AndroidTracker implements Application.ActivityLifecycleCallbacks {
|
|
|
261
645
|
event.put("duration", duration);
|
|
262
646
|
enqueueEvent(event);
|
|
263
647
|
lastPageStartTime = 0;
|
|
648
|
+
flushAsync();
|
|
264
649
|
}
|
|
265
650
|
} catch (Exception e) {
|
|
266
|
-
Log.e(TAG, "onActivityPaused
|
|
651
|
+
Log.e(TAG, "===> [Native Tracker onActivityPaused Error]", e);
|
|
267
652
|
}
|
|
268
653
|
}
|
|
269
654
|
});
|
|
270
655
|
}
|
|
271
656
|
|
|
272
|
-
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
|
|
273
657
|
@Override public void onActivityStarted(Activity activity) {}
|
|
274
658
|
@Override public void onActivityStopped(Activity activity) { flushAsync(); }
|
|
275
659
|
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
|
|
276
660
|
@Override public void onActivityDestroyed(Activity activity) {}
|
|
661
|
+
|
|
662
|
+
// --- Window.Callback 代理包装器 (带 S3 滑动防误触过滤) ---
|
|
663
|
+
private class WindowCallbackWrapper implements Window.Callback {
|
|
664
|
+
private final Window.Callback mWrapped;
|
|
665
|
+
private final Activity mActivity;
|
|
666
|
+
private float downX, downY;
|
|
667
|
+
private long downTime;
|
|
668
|
+
private int touchSlop = -1;
|
|
669
|
+
|
|
670
|
+
public WindowCallbackWrapper(Window.Callback wrapped, Activity activity) {
|
|
671
|
+
this.mWrapped = wrapped;
|
|
672
|
+
this.mActivity = activity;
|
|
673
|
+
if (activity != null) {
|
|
674
|
+
try {
|
|
675
|
+
this.touchSlop = ViewConfiguration.get(activity).getScaledTouchSlop();
|
|
676
|
+
} catch (Exception ignored) {}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
@Override
|
|
681
|
+
public boolean dispatchTouchEvent(MotionEvent event) {
|
|
682
|
+
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
|
683
|
+
downX = event.getRawX();
|
|
684
|
+
downY = event.getRawY();
|
|
685
|
+
downTime = System.currentTimeMillis();
|
|
686
|
+
} else if (event.getAction() == MotionEvent.ACTION_UP) {
|
|
687
|
+
float dx = Math.abs(event.getRawX() - downX);
|
|
688
|
+
float dy = Math.abs(event.getRawY() - downY);
|
|
689
|
+
double distance = Math.hypot(dx, dy);
|
|
690
|
+
long duration = System.currentTimeMillis() - downTime;
|
|
691
|
+
|
|
692
|
+
// S3 过滤:位移小于 touchSlop 且按下时长正常时才判定为点击
|
|
693
|
+
int slopThreshold = touchSlop > 0 ? touchSlop : 24;
|
|
694
|
+
if (distance <= slopThreshold && duration < 1500) {
|
|
695
|
+
try {
|
|
696
|
+
if (mActivity != null && mActivity.getWindow() != null) {
|
|
697
|
+
View decorView = mActivity.getWindow().getDecorView();
|
|
698
|
+
View leaf = findTouchedLeafView(decorView, (int) event.getRawX(), (int) event.getRawY());
|
|
699
|
+
View clickableTarget = null;
|
|
700
|
+
View curr = leaf;
|
|
701
|
+
while (curr != null) {
|
|
702
|
+
if (isClickableTarget(curr)) {
|
|
703
|
+
clickableTarget = curr;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (curr == decorView || !(curr.getParent() instanceof View)) break;
|
|
707
|
+
curr = (View) curr.getParent();
|
|
708
|
+
}
|
|
709
|
+
if (clickableTarget != null) {
|
|
710
|
+
handleNativeViewClick(mActivity, clickableTarget, event.getRawX(), event.getRawY());
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} catch (Exception e) {
|
|
714
|
+
Log.w(TAG, "===> [Native Click Watcher Error]", e);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return mWrapped.dispatchTouchEvent(event);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
@Override public boolean dispatchKeyEvent(KeyEvent event) { return mWrapped.dispatchKeyEvent(event); }
|
|
722
|
+
@Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return mWrapped.dispatchKeyShortcutEvent(event); }
|
|
723
|
+
@Override public boolean dispatchTrackballEvent(MotionEvent event) { return mWrapped.dispatchTrackballEvent(event); }
|
|
724
|
+
@Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return mWrapped.dispatchGenericMotionEvent(event); }
|
|
725
|
+
@Override public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) { return mWrapped.dispatchPopulateAccessibilityEvent(event); }
|
|
726
|
+
@Nullable @Override public View onCreatePanelView(int featureId) { return mWrapped.onCreatePanelView(featureId); }
|
|
727
|
+
@Override public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { return mWrapped.onCreatePanelMenu(featureId, menu); }
|
|
728
|
+
@Override public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) { return mWrapped.onPreparePanel(featureId, view, menu); }
|
|
729
|
+
@Override public boolean onMenuOpened(int featureId, @NonNull Menu menu) { return mWrapped.onMenuOpened(featureId, menu); }
|
|
730
|
+
@Override public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { return mWrapped.onMenuItemSelected(featureId, item); }
|
|
731
|
+
@Override public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) { mWrapped.onWindowAttributesChanged(attrs); }
|
|
732
|
+
@Override public void onContentChanged() { mWrapped.onContentChanged(); }
|
|
733
|
+
@Override public void onWindowFocusChanged(boolean hasFocus) { mWrapped.onWindowFocusChanged(hasFocus); }
|
|
734
|
+
@Override public void onAttachedToWindow() { mWrapped.onAttachedToWindow(); }
|
|
735
|
+
@Override public void onDetachedFromWindow() { mWrapped.onDetachedFromWindow(); }
|
|
736
|
+
@Override public void onPanelClosed(int featureId, @NonNull Menu menu) { mWrapped.onPanelClosed(featureId, menu); }
|
|
737
|
+
@Override public boolean onSearchRequested() { return mWrapped.onSearchRequested(); }
|
|
738
|
+
@Override public boolean onSearchRequested(SearchEvent searchEvent) { return mWrapped.onSearchRequested(searchEvent); }
|
|
739
|
+
@Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { return mWrapped.onWindowStartingActionMode(callback); }
|
|
740
|
+
@Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) { return mWrapped.onWindowStartingActionMode(callback, type); }
|
|
741
|
+
@Override public void onActionModeStarted(ActionMode mode) { mWrapped.onActionModeStarted(mode); }
|
|
742
|
+
@Override public void onActionModeFinished(ActionMode mode) { mWrapped.onActionModeFinished(mode); }
|
|
743
|
+
}
|
|
277
744
|
}
|
|
@@ -28,7 +28,15 @@ import UIKit
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
@objc public func initSDK(serverUrl: String, deviceId: String, userId: String, phone: String, storeName: String) {
|
|
31
|
-
|
|
31
|
+
if !serverUrl.contains("/api/point/task/trackBatch") {
|
|
32
|
+
if serverUrl.hasSuffix("/") {
|
|
33
|
+
self.serverUrl = serverUrl + "api/point/task/trackBatch"
|
|
34
|
+
} else {
|
|
35
|
+
self.serverUrl = serverUrl + "/api/point/task/trackBatch"
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
self.serverUrl = serverUrl
|
|
39
|
+
}
|
|
32
40
|
self.deviceId = deviceId.isEmpty ? (UIDevice.current.identifierForVendor?.uuidString ?? "") : deviceId
|
|
33
41
|
self.userId = userId
|
|
34
42
|
self.phone = phone
|
|
@@ -66,13 +74,23 @@ import UIKit
|
|
|
66
74
|
|
|
67
75
|
private func createBaseEvent(eventType: String, eventName: String) -> [String: Any] {
|
|
68
76
|
sequence += 1
|
|
77
|
+
var extra: [String: Any] = ["platform": "ios"]
|
|
78
|
+
if !phone.isEmpty { extra["phone"] = phone }
|
|
79
|
+
if !storeName.isEmpty { extra["storeName"] = storeName }
|
|
80
|
+
if !userId.isEmpty { extra["userId"] = userId }
|
|
81
|
+
var extraStr = ""
|
|
82
|
+
if let data = try? JSONSerialization.data(withJSONObject: extra), let str = String(data: data, encoding: .utf8) {
|
|
83
|
+
extraStr = str
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
var event: [String: Any] = [
|
|
70
87
|
"eventId": "e_\(Int(Date().timeIntervalSince1970 * 1000))_\(Int.random(in: 1000...9999))",
|
|
71
88
|
"eventType": eventType,
|
|
72
89
|
"eventName": eventName,
|
|
73
90
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
74
91
|
"sequence": sequence,
|
|
75
|
-
"os": "iOS"
|
|
92
|
+
"os": "iOS",
|
|
93
|
+
"extraData": extraStr
|
|
76
94
|
]
|
|
77
95
|
return event
|
|
78
96
|
}
|
|
@@ -106,6 +124,8 @@ import UIKit
|
|
|
106
124
|
var request = URLRequest(url: url)
|
|
107
125
|
request.httpMethod = "POST"
|
|
108
126
|
request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
|
|
127
|
+
request.setValue(appId, forHTTPHeaderField: "appId")
|
|
128
|
+
request.setValue(deviceId, forHTTPHeaderField: "deviceId")
|
|
109
129
|
|
|
110
130
|
let body: [String: Any] = [
|
|
111
131
|
"appId": Int(appId) ?? 13,
|