5757import org .json .JSONArray ;
5858import org .json .JSONException ;
5959import org .json .JSONObject ;
60+ import com .google .android .datatransport .Transform ;
6061
6162import java .io .ByteArrayOutputStream ;
6263import java .io .File ;
@@ -392,7 +393,7 @@ protected void onCreate(Bundle savedInstanceState) {
392393 // Start button click listener
393394 findViewById (R .id .startBtn ).setOnClickListener (v -> {
394395 if (!isLocationEnabled ()) {
395- showMessage ("请开启定位服务后重试 " );
396+ showMessage ("��开启定位服务后重试 " );
396397 startActivity (new Intent (Settings .ACTION_LOCATION_SOURCE_SETTINGS ));
397398 return ;
398399 }
@@ -640,7 +641,11 @@ private void showLeJianDialog() {
640641 }
641642 // 保存账号信息(支持多个账号,phone:password 按行存储)
642643 saveLeJianAccount (phone , password );
643- loginWithOkHttp (phone , password , distance );
644+
645+ // 切换为针对“体适能APP”的登录与上传逻辑
646+ // loginWithOkHttp(phone, password, distance); // 原乐健逻辑
647+ loginTiShiNeng (phone , password , distance ); // 新增的体适能逻辑占位符
648+
644649 dialog .dismiss ();
645650 } catch (Exception e ) {
646651 showToast ("无效的里程输入" );
@@ -830,6 +835,143 @@ private void showToast(String message) {
830835 );
831836 }
832837
838+ /**
839+ * 适配指南:针对“体适能APP”的登录逻辑占位符
840+ *
841+ * 适配新APP通常涉及以下步骤(需要自行抓包分析):
842+ * 1. 抓取“登录”接口:
843+ * - 确定URL(如 https://api.tishineng.com/login)
844+ * - 确定请求方式(POST/GET)
845+ * - 分析请求Header(关键字段:User-Agent, Authorization, Custom-Sign等)
846+ * - 分析请求Body(是否加密?参数名是 username 还是 phone?)
847+ * - 分析响应:获取 Token 或 Cookie,以及 UserID
848+ *
849+ * 2. 抓取“上传跑步/成绩”接口:
850+ * - 确定URL
851+ * - 分析数据结构:是上传轨迹点数组(JSON/GPX),还是仅上传最终里程数值?
852+ * - 分析签名算法:通常会有 sign = MD5(param1 + param2 + salt) 这样的校验
853+ */
854+ public void loginTiShiNeng (String phone , String password , float distance ) {
855+ new Thread (() -> {
856+ try {
857+ // 检查 .so 库是否就绪(通过尝试加载 Transform 类触发)
858+ try {
859+ Transform .INSTANCE .toString ();
860+ } catch (Throwable t ) {
861+ runOnUiThread (() -> new com .google .android .material .dialog .MaterialAlertDialogBuilder (this )
862+ .setTitle ("缺少核心组件" )
863+ .setMessage ("未检测到 libtsnsecret.so 库,无法进行签名加密。\n \n " +
864+ "请从体适能APK中提取 libtsnsecret.so (arm64-v8a) \n " +
865+ "并放入 app/src/main/jniLibs/arm64-v8a/ 目录下。" )
866+ .setPositiveButton ("我知道了" , null )
867+ .show ());
868+ return ;
869+ }
870+
871+ // 构造登录 JSON
872+ JSONObject loginPayload = new JSONObject ();
873+ loginPayload .put ("username" , phone );
874+ loginPayload .put ("password" , password ); // 注意:这里可能需要 MD5 或原样发送,需抓包确认
875+ // loginPayload.put("grant_type", "password"); // OAuth 常见参数,需抓包确认
876+ // loginPayload.put("scope", "all");
877+
878+ // 发送登录请求 (URL 需抓包确认,这里是示例占位)
879+ // Request request = ...
880+ // Response response = ...
881+
882+ // 模拟登录成功获取 Token (仅作演示,实际需解析 response)
883+ Log .d ("TiShiNeng" , "模拟登录流程..." );
884+ String mockToken = "mock_token_" + System .currentTimeMillis ();
885+
886+ // 实际使用时,请替换为真实 Token
887+ // String token = responseJson.getString("access_token");
888+
889+ runOnUiThread (() -> showToast ("体适能登录成功(模拟),准备加密上传..." ));
890+
891+ uploadTiShiNengData (mockToken , distance );
892+
893+ } catch (Exception e ) {
894+ e .printStackTrace ();
895+ runOnUiThread (() -> showToast ("体适能登录异常:" + e .getMessage ()));
896+ }
897+ }).start ();
898+ }
899+
900+ /**
901+ * 适配指南:针对“体适能APP”的数据上传逻辑占位符
902+ */
903+ public void uploadTiShiNengData (String token , float distance ) {
904+ Log .d ("TiShiNeng" , "准备上传数据,距离:" + distance + "km" );
905+ new Thread (() -> {
906+ try {
907+ // 1. 构造原始数据 JSON
908+ // 参考 FuckTSN Hook 到的结构,通常需要包含轨迹点
909+ JSONObject data = new JSONObject ();
910+ data .put ("sportType" , 1 ); // 跑步
911+
912+ // 生成轨迹点与计算数据 (复用LeJian的逻辑,因为物理运动是一样的)
913+ // 注意:体适能可能有特定的字段名,需抓包确认。这里演示通用结构。
914+ double speed = 500.0 ; // 配速
915+ int keepTime = (int )(distance * speed );
916+
917+ data .put ("sportTime" , keepTime );
918+ data .put ("sportRange" , distance * 1000 ); // meters
919+ data .put ("begintime" , System .currentTimeMillis () - keepTime * 1000 );
920+ data .put ("endtime" , System .currentTimeMillis ());
921+ data .put ("speed" , distance * 1000.0 / keepTime );
922+
923+ // 生成轨迹 (Important: TiShiNeng likely validates this)
924+ // data.put("trajectory", generateEvaluatedTrajectory(distance)); // 需实现
925+
926+ String jsonRaw = data .toString ();
927+ String timestamp = String .valueOf (System .currentTimeMillis ());
928+
929+ // 2. 调用核心 native 方法进行加密 (Transform.s)
930+ // s 方法返回数组:[0]=解密key, [1]=加密后的data
931+ String [] encryptedResult = Transform .INSTANCE .s (
932+ getApplicationContext (),
933+ token ,
934+ timestamp ,
935+ jsonRaw
936+ );
937+
938+ if (encryptedResult == null || encryptedResult .length < 2 ) {
939+ throw new RuntimeException ("Native加密失败,返回为空" );
940+ }
941+
942+ String key = encryptedResult [0 ]; // 用于解密的 Key(由服务器使用或客户端解密响应)
943+ String encryptedData = encryptedResult [1 ]; // Body 中实际传输的密文
944+
945+ // 3. 生成签名 (Transform.v)
946+ String sign = Transform .INSTANCE .v (getApplicationContext (), token , timestamp );
947+
948+ Log .d ("TiShiNeng" , "加密成功!\n Key: " + key + "\n Sign: " + sign + "\n Data: " + encryptedData );
949+
950+ // 4. 发送上传请求 (URL 需抓包确认)
951+ /*
952+ Request request = new Request.Builder()
953+ .url("https://api.tishineng.com/v1/upload") // 示例 URL
954+ .header("Authorization", "Bearer " + token)
955+ .header("Sign", sign)
956+ .header("Timestamp", timestamp)
957+ .post(RequestBody.create(MediaType.parse("application/json"), encryptedData))
958+ .build();
959+
960+ // 执行请求...
961+ */
962+
963+ Thread .sleep (1000 );
964+ runOnUiThread (() -> showLongerToast ("🎉 本地加密签名成功!(模拟上传) \n 请抓包获取真实 URL 完成最后一步。" ));
965+
966+ } catch (UnsatisfiedLinkError e ) {
967+ runOnUiThread (() -> showToast ("错误:找不到 libtsnsecret.so,请确保库文件已放入 jniLibs" ));
968+ } catch (Exception e ) {
969+ e .printStackTrace ();
970+ runOnUiThread (() -> showToast ("上传适配异常:" + e .getMessage ()));
971+ }
972+ }).start ();
973+ }
974+
833975 public void loginWithOkHttp (String phone , String password , float distance ) {
834976 new Thread (() -> {
835977 try {
@@ -901,7 +1043,7 @@ public void loginWithOkHttp(String phone, String password, float distance) {
9011043 semesterId = getSemesterId (token );
9021044
9031045 if (token .isEmpty () || id == null || schoolId == null || semesterId == null ) {
904- runOnUiThread (() -> showToast ("登录成功,但缺少必要参数 ,上传取消" ));
1046+ runOnUiThread (() -> showToast ("登录成功,但缺少��要参数 ,上传取消" ));
9051047 return ;
9061048 }
9071049
@@ -1182,7 +1324,6 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
11821324
11831325 if (requestCode == 999 ) { // 通知权限回调
11841326 if (grantResults .length > 0 && grantResults [0 ] == PackageManager .PERMISSION_GRANTED ) {
1185- // ✅ 统一使用 Snackbar 样式的 showMessage
11861327 showMessage ("✅ 通知权限已授权" );
11871328 } else {
11881329 showPillMessage ("未授权通知,可能看不到后台记录" , true );
@@ -1206,7 +1347,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
12061347 showMessage ("✅ 服务权限已获取,后台记录已开启" );
12071348 }
12081349 } else {
1209- showPillMessage ("权限不足,无法准确记录轨迹 " , true );
1350+ showPillMessage ("权限不足,无法准确记录���迹 " , true );
12101351 new com .google .android .material .dialog .MaterialAlertDialogBuilder (this )
12111352 .setTitle ("权限说明" )
12121353 .setMessage ("轨迹记录需要后台定位权限,否则App切入后台后记录会中断。" )
0 commit comments