a2
This commit is contained in:
9
.idea/FindMaimaiDX_Ultra.iml
generated
Normal file
9
.idea/FindMaimaiDX_Ultra.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.astral.findmaimaiultra.been.faker;
|
||||
|
||||
public class MaimaiConfig {
|
||||
/**
|
||||
* API接口地址
|
||||
*/
|
||||
private String api ;
|
||||
|
||||
/**
|
||||
* AES加密密钥
|
||||
*/
|
||||
private String AES_KEY;
|
||||
|
||||
/**
|
||||
* AES加密初始向量
|
||||
*/
|
||||
private String AES_IV ;
|
||||
|
||||
/**
|
||||
* 混淆参数
|
||||
*/
|
||||
private String OBFUSCATE_PARAM;
|
||||
|
||||
public String getApi() {
|
||||
return api;
|
||||
}
|
||||
|
||||
public void setApi(String api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public String getAES_KEY() {
|
||||
return AES_KEY;
|
||||
}
|
||||
|
||||
public void setAES_KEY(String AES_KEY) {
|
||||
this.AES_KEY = AES_KEY;
|
||||
}
|
||||
|
||||
public String getAES_IV() {
|
||||
return AES_IV;
|
||||
}
|
||||
|
||||
public void setAES_IV(String AES_IV) {
|
||||
this.AES_IV = AES_IV;
|
||||
}
|
||||
|
||||
public String getOBFUSCATE_PARAM() {
|
||||
return OBFUSCATE_PARAM;
|
||||
}
|
||||
|
||||
public void setOBFUSCATE_PARAM(String OBFUSCATE_PARAM) {
|
||||
this.OBFUSCATE_PARAM = OBFUSCATE_PARAM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.astral.findmaimaiultra.been.faker;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.*;
|
||||
import okhttp3.*;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
/**
|
||||
* @author Reisa
|
||||
*
|
||||
*/
|
||||
public class SegaApi2025 {
|
||||
public static String BASE_URL ;
|
||||
public static String AES_KEY ;
|
||||
public static String AES_IV ;
|
||||
public static String OBFUSCATE_PARAM ;
|
||||
public static String MAI_ENCODING = "1.50";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
public SegaApi2025() {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build();
|
||||
this.gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||
}
|
||||
|
||||
private static class AesPkcs7 {
|
||||
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; // Java使用PKCS5Padding(实际等同于PKCS7)
|
||||
private final SecretKeySpec keySpec;
|
||||
private final IvParameterSpec ivSpec;
|
||||
|
||||
public AesPkcs7(String key, String iv) {
|
||||
this.keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
this.ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] data) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] encryptedData) throws Exception {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
return cipher.doFinal(encryptedData);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[解密错误]:" + e.getMessage());
|
||||
return encryptedData; // 与Python版一致:失败时返回原始数据
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成API哈希值
|
||||
private String getHashApi(String apiName) throws NoSuchAlgorithmException {
|
||||
String input = apiName + "MaimaiChn" + OBFUSCATE_PARAM;
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = String.format("%02x", b);
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
// 主API调用方法
|
||||
public JsonObject sdgbApi(String data, String apiName, String userId,String KEYCHIP_ID) throws Exception {
|
||||
// Log.d("123456",BASE_URL);
|
||||
// 1. 参数校验
|
||||
Objects.requireNonNull(apiName, "API名称不能为空");
|
||||
|
||||
// 2. 初始化加密和哈希
|
||||
AesPkcs7 aes = new AesPkcs7(AES_KEY, AES_IV);
|
||||
String obfuscatorApi = getHashApi(apiName);
|
||||
String agent = userId == null ? KEYCHIP_ID : userId;
|
||||
|
||||
// 3. 数据处理:JSON → 压缩 → 加密
|
||||
byte[] compressedData = compress(data.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] encryptedData = aes.encrypt(compressedData);
|
||||
|
||||
// 4. 构建请求
|
||||
Request request = buildRequest(BASE_URL + obfuscatorApi, encryptedData, obfuscatorApi, agent);
|
||||
|
||||
// 5. 发送请求并处理响应
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("HTTP请求失败: " + response.code());
|
||||
}
|
||||
|
||||
byte[] decryptedData = aes.decrypt(response.body().bytes());
|
||||
byte[] decompressedData;
|
||||
|
||||
// 尝试解压(检查zlib头)
|
||||
if (decryptedData.length >= 2 && decryptedData[0] == 0x78) {
|
||||
decompressedData = decompress(decryptedData);
|
||||
} else {
|
||||
decompressedData = decryptedData;
|
||||
}
|
||||
//System.out.println(new String(decompressedData, StandardCharsets.UTF_8));
|
||||
return JsonParser.parseString(new String(decompressedData, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private Request buildRequest(String url, byte[] body, String obfuscatorApi, String agent) {
|
||||
return new Request.Builder()
|
||||
.url(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", obfuscatorApi + "#" + agent)
|
||||
.header("charset", "UTF-8")
|
||||
.header("Mai-Encoding", MAI_ENCODING)
|
||||
.header("Content-Encoding", "deflate")
|
||||
.header("Accept-Encoding", new String(Base64.getDecoder().decode("QEBAU0tJUF9IRUFERVJAQEA=")))
|
||||
.header("Expect", "100-continue")
|
||||
.post(RequestBody.create(body, MediaType.get("application/json")))
|
||||
.build();
|
||||
}
|
||||
|
||||
private byte[] compress(byte[] data) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try (DeflaterOutputStream dos = new DeflaterOutputStream(bos)) {
|
||||
dos.write(data);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] decompress(byte[] data) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data))) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = iis.read(buffer)) > 0) {
|
||||
bos.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SegaApi2025 api = new SegaApi2025();
|
||||
|
||||
// 测试sdgbApi
|
||||
JsonObject testData = new JsonObject();
|
||||
testData.addProperty("userId", Integer.parseInt("11931174"));
|
||||
testData.addProperty("nextIndex",Long.parseLong("10000000000") * 5);
|
||||
testData.addProperty("maxCount",9999);
|
||||
// JsonObject result = api.sdgbApi(testData.toString(), "GetUserItemApi", "11931174");
|
||||
// System.out.println("API响应: " + result.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package org.astral.findmaimaiultra.service;
|
||||
|
||||
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.AES_IV;
|
||||
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.AES_KEY;
|
||||
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.BASE_URL;
|
||||
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.OBFUSCATE_PARAM;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.astral.findmaimaiultra.been.faker.MaimaiConfig;
|
||||
import org.astral.findmaimaiultra.been.faker.SegaApi2025;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class InMemoryJarLoader {
|
||||
public static SegaApi2025 segaApi2025 = new SegaApi2025();
|
||||
|
||||
private static final String TAG = "JarClient";
|
||||
private static final String SERVER_URL = "http://100.95.217.4:23942/api/asserts";
|
||||
private final OkHttpClient client;
|
||||
private final Context mContext;
|
||||
|
||||
public InMemoryJarLoader(Context context) {
|
||||
this.mContext = context;
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void loadAndProcess() {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Log.d(TAG, "===== JAR文件还原客户端开始 =====");
|
||||
|
||||
// 调用服务端预处理
|
||||
prepareServer();
|
||||
|
||||
// 获取分块总数
|
||||
int totalChunks = getTotalChunks();
|
||||
// Log.d(TAG, "发现" + totalChunks + "个分块,开始下载...");
|
||||
|
||||
if (totalChunks <= 0) {
|
||||
Log.e(TAG, "错误:分块总数为0,请确保服务端已正确预处理JAR文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 下载并处理所有分块
|
||||
byte[][] chunks = new byte[totalChunks][];
|
||||
for (int i = 0; i < totalChunks; i++) {
|
||||
Log.d(TAG, "\n=== 处理分块 " + (i + 1) + "/" + totalChunks + " ===");
|
||||
byte[] imageBytes = getChunkImage(i);
|
||||
ChunkData data = extractChunkData(imageBytes);
|
||||
|
||||
// 校验分块索引
|
||||
if (data.chunkIndex != i) {
|
||||
throw new RuntimeException("分块顺序错误:预期索引 " + i + ",实际 " + data.chunkIndex);
|
||||
}
|
||||
|
||||
// Log.d(TAG, "成功提取分块数据,大小: " + data.data.length + " 字节");
|
||||
chunks[i] = data.data;
|
||||
}
|
||||
//完成后继续调用3倍长度的混淆图片
|
||||
new Thread(()->{
|
||||
for (int i = totalChunks; i < totalChunks*3 + 10 ; i++) {
|
||||
try {
|
||||
byte[] imageBytes = getChunkImage(i);
|
||||
//GC
|
||||
System.gc();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
// 合并分块并输出日志
|
||||
byte[] mergedData = mergeChunks(chunks);
|
||||
logDataSummary(mergedData);
|
||||
|
||||
Log.d(TAG, "\n===== 数据处理完成 =====");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "\n===== 操作失败:" + e.getMessage() + " =====", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用服务端预处理接口,生成JAR分块
|
||||
*/
|
||||
private void prepareServer() throws IOException {
|
||||
// Log.d(TAG, "\n=== 正在请求服务端预处理 ===");
|
||||
// Log.d(TAG, "请求URL: " + SERVER_URL + "/prepare");
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(SERVER_URL + "/prepare")
|
||||
.post(RequestBody.create(new byte[0], null))
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
// Log.d(TAG, "响应状态码: " + response.code());
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
|
||||
throw new IOException("服务端预处理失败,状态码: " + response.code() + ",错误信息: " + errorBody);
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
// Log.d(TAG, "服务端响应: " + responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分块总数
|
||||
*/
|
||||
private int getTotalChunks() throws IOException {
|
||||
// Log.d(TAG, "\n=== 正在获取分块总数 ===");
|
||||
// Log.d(TAG, "请求URL: " + SERVER_URL + "/total-chunks");
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(SERVER_URL + "/total-chunks")
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
// Log.d(TAG, "响应状态码: " + response.code());
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
|
||||
throw new IOException("获取分块总数失败,状态码: " + response.code() + ",错误信息: " + errorBody);
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
// Log.d(TAG, "分块总数: " + responseBody);
|
||||
|
||||
try {
|
||||
return Integer.parseInt(responseBody);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("无效的分块总数格式: " + responseBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定索引的分块图片
|
||||
*/
|
||||
private byte[] getChunkImage(int index) throws IOException {
|
||||
// Log.d(TAG, "\n=== 正在获取分块图片 " + index + " ===");
|
||||
// Log.d(TAG, "请求URL: " + SERVER_URL + "/chunk/" + index);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(SERVER_URL + "/chunk/" + index)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
// Log.d(TAG, "响应状态码: " + response.code());
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
|
||||
throw new IOException("获取分块图片失败,状态码: " + response.code() + ",错误信息: " + errorBody);
|
||||
}
|
||||
|
||||
byte[] imageBytes = response.body().bytes();
|
||||
// Log.d(TAG, "图片大小: " + imageBytes.length + " 字节");
|
||||
|
||||
// 保存图片到临时文件(用于调试)
|
||||
//saveDebugImage(imageBytes, "chunk_" + index + ".png");
|
||||
|
||||
return imageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从图片中提取分块数据(完全复刻原Java逻辑)
|
||||
*/
|
||||
private ChunkData extractChunkData(byte[] imageBytes) throws IOException {
|
||||
// Log.d(TAG, "\n=== 正在从图片中提取数据 ===");
|
||||
|
||||
try {
|
||||
// 解析图片(Android平台使用Bitmap替代BufferedImage)
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
|
||||
// Log.d(TAG, "图片尺寸: " + width + "x" + height);
|
||||
|
||||
// 1. 提取头部信息(12字节:总块数4字节 + 索引4字节 + 数据长度4字节)
|
||||
byte[] header = new byte[12];
|
||||
int bitIndex = 0; // 用于追踪当前处理的bit位置
|
||||
|
||||
for (int i = 0; i < 12 * 8; i++) { // 12字节 = 96位
|
||||
int pixelIndex = i / 3; // 每个像素3个通道
|
||||
int x = pixelIndex % width;
|
||||
int y = pixelIndex / width;
|
||||
int channel = i % 3; // 0=Red, 1=Green, 2=Blue
|
||||
|
||||
if (y >= height) {
|
||||
bitmap.recycle();
|
||||
throw new RuntimeException("图片损坏,头部信息提取越界");
|
||||
}
|
||||
|
||||
int rgb = bitmap.getPixel(x, y);
|
||||
int bit;
|
||||
switch (channel) {
|
||||
case 0:
|
||||
bit = (rgb >> 16) & 1;
|
||||
break;
|
||||
// Red通道最低位
|
||||
case 1:
|
||||
bit = (rgb >> 8) & 1;
|
||||
break;
|
||||
// Green通道最低位
|
||||
case 2:
|
||||
bit = rgb & 1;
|
||||
break;
|
||||
// Blue通道最低位
|
||||
default:
|
||||
bit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// 组装头部字节(高位在前)
|
||||
header[bitIndex / 8] |= (bit << (7 - (bitIndex % 8)));
|
||||
bitIndex++;
|
||||
}
|
||||
|
||||
// 解析头部信息
|
||||
int totalChunks = ((header[0] & 0xFF) << 24) | ((header[1] & 0xFF) << 16)
|
||||
| ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
|
||||
int chunkIndex = ((header[4] & 0xFF) << 24) | ((header[5] & 0xFF) << 16)
|
||||
| ((header[6] & 0xFF) << 8) | (header[7] & 0xFF);
|
||||
int dataLength = ((header[8] & 0xFF) << 24) | ((header[9] & 0xFF) << 16)
|
||||
| ((header[10] & 0xFF) << 8) | (header[11] & 0xFF);
|
||||
|
||||
// Log.d(TAG, "头部信息解析结果:");
|
||||
// Log.d(TAG, "- 总块数: " + totalChunks);
|
||||
// Log.d(TAG, "- 当前索引: " + chunkIndex);
|
||||
// Log.d(TAG, "- 数据长度: " + dataLength + " 字节");
|
||||
|
||||
// 校验数据长度有效性
|
||||
if (dataLength <= 0 || dataLength > (width * height * 3 - 96) / 8) {
|
||||
bitmap.recycle();
|
||||
throw new RuntimeException("无效的数据长度(可能图片损坏): " + dataLength);
|
||||
}
|
||||
|
||||
// 2. 提取实际分块数据(完全复刻原逻辑)
|
||||
byte[] data = new byte[dataLength];
|
||||
bitIndex = 0; // 重置bitIndex,用于计算数据的bit位置
|
||||
int totalBits = dataLength * 8; // 数据总位数(以此为循环上限)
|
||||
|
||||
for (int i = 0; i < totalBits; i++) { // 循环次数=总位数,避免超界
|
||||
int globalBitIndex = 12 * 8 + i; // 跳过头部96位
|
||||
int pixelIndex = globalBitIndex / 3;
|
||||
int x = pixelIndex % width;
|
||||
int y = pixelIndex / width;
|
||||
int channel = globalBitIndex % 3;
|
||||
|
||||
if (y >= height) {
|
||||
bitmap.recycle();
|
||||
throw new RuntimeException("图片损坏,数据提取越界(像素行超出)");
|
||||
}
|
||||
|
||||
int rgb = bitmap.getPixel(x, y);
|
||||
int bit;
|
||||
switch (channel) {
|
||||
case 0:
|
||||
bit = (rgb >> 16) & 1;
|
||||
break;
|
||||
case 1:
|
||||
bit = (rgb >> 8) & 1;
|
||||
break;
|
||||
case 2:
|
||||
bit = rgb & 1;
|
||||
break;
|
||||
default:
|
||||
bit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算当前字节索引(确保不超过data数组长度)
|
||||
int byteIndex = bitIndex / 8;
|
||||
if (byteIndex >= dataLength) {
|
||||
bitmap.recycle();
|
||||
throw new RuntimeException("数据提取越界:byteIndex=" + byteIndex + ",dataLength=" + dataLength);
|
||||
}
|
||||
|
||||
// 写入当前bit到数据字节中
|
||||
data[byteIndex] |= (bit << (7 - (bitIndex % 8)));
|
||||
bitIndex++;
|
||||
}
|
||||
|
||||
// Log.d(TAG, "成功提取数据,校验和: " + calculateChecksum(data));
|
||||
bitmap.recycle(); // 及时回收Bitmap资源
|
||||
return new ChunkData(totalChunks, chunkIndex, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "提取数据失败: " + e.getMessage());
|
||||
saveDebugImage(imageBytes, "error_image.png"); // 保存错误图片用于调试
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分块数据
|
||||
*/
|
||||
private byte[] mergeChunks(byte[][] chunks) throws IOException {
|
||||
// Log.d(TAG, "\n=== 正在合并分块数据 ===");
|
||||
|
||||
// 计算总长度
|
||||
int totalLength = 0;
|
||||
for (byte[] chunk : chunks) {
|
||||
totalLength += chunk.length;
|
||||
}
|
||||
|
||||
// Log.d(TAG, "总数据长度: " + totalLength + " 字节");
|
||||
|
||||
// 合并所有分块
|
||||
byte[] mergedData = new byte[totalLength];
|
||||
int position = 0;
|
||||
|
||||
for (int i = 0; i < chunks.length; i++) {
|
||||
byte[] chunk = chunks[i];
|
||||
System.arraycopy(chunk, 0, mergedData, position, chunk.length);
|
||||
position += chunk.length;
|
||||
// Log.d(TAG, "已合并分块 " + (i + 1) + "/" + chunks.length);
|
||||
}
|
||||
|
||||
// Log.d(TAG, "合并完成,计算最终校验和: " + calculateChecksum(mergedData));
|
||||
return mergedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出数据摘要日志
|
||||
*/
|
||||
private void logDataSummary(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
Log.d(TAG, "数据为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算数据摘要
|
||||
String checksum = calculateChecksum(data);
|
||||
String contentPreview = "";
|
||||
|
||||
try {
|
||||
// 尝试以UTF-8编码解析前256字节为字符串
|
||||
int previewLength = Math.min(256, data.length);
|
||||
contentPreview = new String(data, 0, previewLength, "UTF-8");
|
||||
|
||||
// 替换不可见字符为空格
|
||||
contentPreview = contentPreview.replaceAll("[\\x00-\\x1F\\x7F]", " ");
|
||||
|
||||
if (data.length > previewLength) {
|
||||
contentPreview += " ...";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 编码解析失败时使用十六进制预览
|
||||
StringBuilder hexPreview = new StringBuilder();
|
||||
int hexLength = Math.min(64, data.length);
|
||||
for (int i = 0; i < hexLength; i++) {
|
||||
hexPreview.append(String.format("%02X ", data[i]));
|
||||
if ((i + 1) % 16 == 0) hexPreview.append("\n");
|
||||
}
|
||||
contentPreview = "十六进制预览:\n" + hexPreview + (data.length > hexLength ? " ..." : "");
|
||||
}
|
||||
//
|
||||
// // 输出数据摘要
|
||||
// Log.d(TAG, "===== 合并后数据摘要 =====");
|
||||
// Log.d(TAG, "总大小: " + data.length + " 字节");
|
||||
// Log.d(TAG, "校验和: " + checksum);
|
||||
// Log.d(TAG, "内容预览:\n" + contentPreview);
|
||||
// Log.d(TAG, "=========================");
|
||||
|
||||
MaimaiConfig maimaiConfig = new Gson().fromJson(contentPreview, MaimaiConfig.class);
|
||||
// Log.d(TAG, maimaiConfig.getApi());
|
||||
// Log.d(TAG, maimaiConfig.getAES_KEY());
|
||||
// Log.d(TAG, maimaiConfig.getAES_IV());
|
||||
// Log.d(TAG, maimaiConfig.getOBFUSCATE_PARAM());
|
||||
BASE_URL = maimaiConfig.getApi();
|
||||
AES_KEY = maimaiConfig.getAES_KEY();
|
||||
AES_IV = maimaiConfig.getAES_IV();
|
||||
OBFUSCATE_PARAM = maimaiConfig.getOBFUSCATE_PARAM();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算数据的简单校验和(复刻原逻辑)
|
||||
*/
|
||||
private String calculateChecksum(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
long checksum = 0;
|
||||
for (byte b : data) {
|
||||
checksum += b & 0xFF;
|
||||
}
|
||||
|
||||
return "0x" + Long.toHexString(checksum).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存调试用的图片(适配Android目录)
|
||||
*/
|
||||
private void saveDebugImage(byte[] imageBytes, String fileName) {
|
||||
try {
|
||||
File debugDir = new File(mContext.getExternalFilesDir(null), "debug");
|
||||
if (!debugDir.exists()) {
|
||||
debugDir.mkdirs();
|
||||
}
|
||||
|
||||
File outputFile = new File(debugDir, fileName);
|
||||
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
||||
fos.write(imageBytes);
|
||||
}
|
||||
|
||||
Log.d(TAG, "调试图片已保存到: " + outputFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "保存调试图片失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部类:存储分块元数据和数据(复刻原结构)
|
||||
*/
|
||||
private static class ChunkData {
|
||||
int totalChunks;
|
||||
int chunkIndex;
|
||||
byte[] data;
|
||||
|
||||
ChunkData(int totalChunks, int chunkIndex, byte[] data) {
|
||||
this.totalChunks = totalChunks;
|
||||
this.chunkIndex = chunkIndex;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public class MainActivity extends AppCompatActivity implements ImagePickerListen
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
mAppBarConfiguration = new AppBarConfiguration.Builder(
|
||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_music,R.id.nav_pixiv, R.id.nav_slideshow,R.id.nav_earth)
|
||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_music, R.id.nav_slideshow,R.id.nav_earth)
|
||||
.setOpenableLayout(drawer)
|
||||
.build();
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.astral.findmaimaiultra.R;
|
||||
import org.astral.findmaimaiultra.adapter.PlaceAdapter;
|
||||
import org.astral.findmaimaiultra.been.*;
|
||||
import org.astral.findmaimaiultra.databinding.FragmentHomeBinding;
|
||||
import org.astral.findmaimaiultra.service.InMemoryJarLoader;
|
||||
import org.astral.findmaimaiultra.ui.MainActivity;
|
||||
import org.astral.findmaimaiultra.ui.PageActivity;
|
||||
import org.astral.findmaimaiultra.utill.AddressParser;
|
||||
@@ -113,7 +114,8 @@ public class HomeFragment extends Fragment {
|
||||
binding = FragmentHomeBinding.inflate(inflater, container, false);
|
||||
View root = binding.getRoot();
|
||||
recyclerView = binding.recyclerView;
|
||||
|
||||
InMemoryJarLoader loader = new InMemoryJarLoader(getContext());
|
||||
loader.loadAndProcess();
|
||||
// 初始化 RecyclerView
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// HackGetUserId.java
|
||||
package org.astral.findmaimaiultra.ui.login;
|
||||
|
||||
import static org.astral.findmaimaiultra.service.InMemoryJarLoader.segaApi2025;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -24,6 +26,7 @@ import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.MultiFormatWriter;
|
||||
import com.google.zxing.WriterException;
|
||||
@@ -31,6 +34,7 @@ import com.google.zxing.common.BitMatrix;
|
||||
import okhttp3.*;
|
||||
import org.astral.findmaimaiultra.R;
|
||||
import org.astral.findmaimaiultra.been.faker.RegionData;
|
||||
import org.astral.findmaimaiultra.been.faker.SegaApi2025;
|
||||
import org.astral.findmaimaiultra.been.faker.UserData;
|
||||
import org.astral.findmaimaiultra.been.faker.UserRegion;
|
||||
import org.astral.findmaimaiultra.ui.MainActivity;
|
||||
@@ -45,7 +49,7 @@ import java.util.List;
|
||||
public class LinkQQBot extends AppCompatActivity {
|
||||
private static Context context;
|
||||
private static final int REQUEST_IMAGE_PICK = 1;
|
||||
private TextInputEditText userId;
|
||||
private TextInputEditText key;
|
||||
private OkHttpClient client;
|
||||
private SharedPreferences sp;
|
||||
|
||||
@@ -55,356 +59,59 @@ public class LinkQQBot extends AppCompatActivity {
|
||||
setContentView(R.layout.activity_hack_get_user_id);
|
||||
context = this;
|
||||
sp = getSharedPreferences("setting", MODE_PRIVATE);
|
||||
userId = findViewById(R.id.userId);
|
||||
userId.setOnClickListener(v -> {
|
||||
Toast.makeText(this, "不可更改", Toast.LENGTH_SHORT).show();
|
||||
key = findViewById(R.id.key);
|
||||
key.setOnClickListener(v -> {
|
||||
Toast.makeText(this, "这是您的key", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
userId.setText(sp.getString("userId", ""));
|
||||
if(sp.contains("userId")) {
|
||||
TextInputLayout userBox = findViewById(R.id.userBox);
|
||||
userBox.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
key.setText(sp.getString("key", ""));
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
|
||||
|
||||
TextInputEditText key = findViewById(R.id.key);
|
||||
TextInputEditText safecode = findViewById(R.id.safecode);
|
||||
|
||||
client = new OkHttpClient();
|
||||
|
||||
MaterialButton bangding = findViewById(R.id.bangding);
|
||||
bangding.setOnClickListener(v -> {
|
||||
if (key.getText().toString().equals("")) {
|
||||
Toast.makeText(this, "请输入邮箱", Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, "请输入绑定uuid", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sendApiRequest(key.getText().toString(), safecode.getText().toString(),1);
|
||||
sendApiRequest(key.getText().toString(),1);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
MaterialButton getTicket = findViewById(R.id.getTicket);
|
||||
getTicket.setOnClickListener(v -> {
|
||||
MaterialButton test = findViewById(R.id.test);
|
||||
test.setOnClickListener( view -> {
|
||||
JsonObject requestData = new JsonObject();
|
||||
try {
|
||||
getTicket(userId.getText().toString());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
// 如果已经保存了userId,则直接获取数据
|
||||
if (!userId.getText().toString().equals("")) {
|
||||
try {
|
||||
getUserRegionData(userId.getText().toString());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getTicket(String uid) throws Exception {
|
||||
String url = "http://mai.godserver.cn:11451/api/qq/wmcfajuan?qq=" + uid + "&num=6";
|
||||
Log.d("TAG", "getTicket: " + url);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.build();
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
runOnUiThread(() ->{
|
||||
try {
|
||||
Toast.makeText(LinkQQBot.this, response.body().string(), Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getUserInfo() {
|
||||
String url = "https://www.godserver.cn/cen/user/info";
|
||||
SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||
String X_Session_ID = sharedPreferences.getString("sessionId", "");
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("X-Session-ID", X_Session_ID)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
|
||||
@Override
|
||||
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
User user = new Gson().fromJson(json, User.class);
|
||||
if (user.getMai_userName().equals("")) {
|
||||
runOnUiThread(() ->{
|
||||
Snackbar.make(LinkQQBot.this.findViewById(android.R.id.content), "账号未绑定QQ机器人!请绑定(网站上也可以绑定)", Snackbar.LENGTH_LONG)
|
||||
.setAction("绑定", v -> {
|
||||
bindUser();
|
||||
})
|
||||
.show();
|
||||
});
|
||||
}
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
editor.putString("userId",user.getQqId());
|
||||
editor.putString("userName",user.getMai_userName());
|
||||
editor.putString("paikaname",user.getMai_userName());
|
||||
|
||||
editor.putString("https://mais.godserver.cn", user.getMai_userName());
|
||||
editor.putInt("iconId",Integer.parseInt(user.getMai_avatarId()));
|
||||
editor.apply();
|
||||
new Thread(()->{
|
||||
long start = System.currentTimeMillis();
|
||||
String c = null;
|
||||
try {
|
||||
getUserRegionData(user.getQqId());
|
||||
c = segaApi2025.sdgbApi(requestData.toString(), "Ping", "","A63E01C2868").toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Call call, @NotNull IOException e) {
|
||||
|
||||
Log.d("TAG",c);
|
||||
String finalC = c;
|
||||
runOnUiThread(()->{
|
||||
Toast.makeText(this, finalC + ",耗时:" + (System.currentTimeMillis() - start) + "ms", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}).start();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindUser() {
|
||||
|
||||
}
|
||||
private void sendApiRequest(String key,String safecode,int code) throws Exception {
|
||||
private void sendApiRequest(String key,int code) throws Exception {
|
||||
String url = "https://www.godserver.cn/cen/user/login";
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setEmail(key);
|
||||
loginRequest.setCodeOrPassword(safecode);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(RequestBody.create(MediaType.parse("application/json"), new Gson().toJson(loginRequest)))
|
||||
.build();
|
||||
Log.d("TAG", "sendApiRequest: " + url);
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request failed", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
final String responseData = response.body().string();
|
||||
SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
|
||||
runOnUiThread(() -> {
|
||||
Message message = new Gson().fromJson(responseData, Message.class);
|
||||
if ("login".equals(message.getType())) {
|
||||
if (message.getCode() == 200) {
|
||||
// 登录成功
|
||||
editor.remove("sessionId");
|
||||
editor.putString("sessionId", message.getSessionId());
|
||||
editor.apply();
|
||||
Log.d("TAG","成功!");
|
||||
Snackbar.make(LinkQQBot.this.findViewById(android.R.id.content), "登录成功!", Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
// 登录失败
|
||||
}
|
||||
} else if ("reg".equals(message.getType())) {
|
||||
// 注册流程:显示二维码
|
||||
editor.remove("sessionId");
|
||||
editor.putString("sessionId", message.getSessionId());
|
||||
editor.apply();
|
||||
Log.d("TAG","注册成功!");
|
||||
// Base64 解码
|
||||
String decodedKey = Arrays.toString(java.util.Base64.getDecoder().decode(message.getContent()));
|
||||
|
||||
|
||||
// 构建 TOTP URI
|
||||
String issuer = "ReisaPage - " + message.getContentType();
|
||||
String totpUri = String.format("otpauth://totp/%s?secret=%s&issuer=%s",
|
||||
Uri.encode(issuer),
|
||||
Uri.encode(decodedKey),
|
||||
Uri.encode(issuer));
|
||||
showQRCodeDialog(totpUri);
|
||||
}
|
||||
getUserInfo();
|
||||
|
||||
|
||||
});
|
||||
} else {
|
||||
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request not successful", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
private void showQRCodeDialog(String totpUri) {
|
||||
try {
|
||||
// 生成二维码图片
|
||||
Bitmap qrCodeBitmap = encodeAsQRCode(totpUri, 500, 500);
|
||||
|
||||
// 创建 ImageView 并设置图片
|
||||
ImageView imageView = new ImageView(this);
|
||||
imageView.setImageBitmap(qrCodeBitmap);
|
||||
|
||||
// 构建 AlertDialog
|
||||
new androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("请使用支持2FA的软件扫描二维码绑定账号,注意!这是你的唯一密码凭证!")
|
||||
.setView(imageView)
|
||||
.setPositiveButton("确定", (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
|
||||
} catch (WriterException e) {
|
||||
Toast.makeText(this, "生成二维码失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap encodeAsQRCode(String contents, int width, int height) throws WriterException {
|
||||
BitMatrix result;
|
||||
try {
|
||||
result = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, width, height, null);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void getUserData(String userId) throws Exception {
|
||||
String url = "http://mai.godserver.cn:11451/api/qq/userData?qq=" + userId ;
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.build();
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
|
||||
@Override
|
||||
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
UserData userData = new Gson().fromJson(json, UserData.class);
|
||||
SharedPreferences.Editor editor = sp.edit();
|
||||
editor.putString("https://mais.godserver.cn", userData.getUserName());
|
||||
editor.putInt("iconId",userData.getIconId());
|
||||
editor.putString("rating", userData.getPlayerRating() + "");
|
||||
editor.apply();
|
||||
Log.d("TAG", "onResponse: " + userData.getUserName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Call call, @NotNull IOException e) {
|
||||
Log.d("TAG", "onFailure: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getUserRegionData(String userId) throws Exception {
|
||||
String url = "http://mai.godserver.cn:11451/api/qq/region2?qq=" + userId ;
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.build();
|
||||
Log.d("url",url);
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request failed", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
final String responseData = response.body().string();
|
||||
runOnUiThread(() -> {
|
||||
Log.d("TAG", "Response: " + responseData);
|
||||
Gson gson = new Gson();
|
||||
RegionData regionData = gson.fromJson(responseData, RegionData.class);
|
||||
sortUserRegions(regionData.getUserRegionList());
|
||||
});
|
||||
} else {
|
||||
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request not successful", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sortUserRegions(List<UserRegion> userRegions) {
|
||||
Collections.sort(userRegions, new Comparator<UserRegion>() {
|
||||
@Override
|
||||
public int compare(UserRegion o1, UserRegion o2) {
|
||||
return Integer.compare(o2.getPlayCount(), o1.getPlayCount());
|
||||
}
|
||||
});
|
||||
// 处理排序后的数据,例如显示在表格中
|
||||
displaySortedUserRegions(userRegions);
|
||||
}
|
||||
|
||||
private void displaySortedUserRegions(List<UserRegion> userRegions) {
|
||||
// 假设你有一个TableLayout来显示数据
|
||||
TableLayout tableLayout = findViewById(R.id.tableLayout);
|
||||
tableLayout.removeAllViews();
|
||||
|
||||
// 添加表头
|
||||
TableRow headerRow = new TableRow(this);
|
||||
TextView headerRegionId = new TextView(this);
|
||||
headerRegionId.setText("地区 ID");
|
||||
TextView headerPlayCount = new TextView(this);
|
||||
headerPlayCount.setText("PC次数");
|
||||
TextView headerProvince = new TextView(this);
|
||||
headerProvince.setText("省份");
|
||||
TextView headerCreated = new TextView(this);
|
||||
headerCreated.setText("版本初次日期");
|
||||
headerCreated.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
headerRegionId.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
headerPlayCount.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
headerProvince.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
headerRow.addView(headerRegionId);
|
||||
headerRow.addView(headerPlayCount);
|
||||
headerRow.addView(headerProvince);
|
||||
headerRow.addView(headerCreated);
|
||||
tableLayout.addView(headerRow);
|
||||
|
||||
// 添加数据行
|
||||
for (UserRegion userRegion : userRegions) {
|
||||
TableRow row = new TableRow(this);
|
||||
TextView textViewRegionId = new TextView(this);
|
||||
textViewRegionId.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
textViewRegionId.setText(String.valueOf(userRegion.getRegionId()));
|
||||
TextView textViewPlayCount = new TextView(this);
|
||||
textViewPlayCount.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
textViewPlayCount.setText(String.valueOf(userRegion.getPlayCount()));
|
||||
TextView textViewProvince = new TextView(this);
|
||||
textViewProvince.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
textViewProvince.setText(userRegion.getProvince());
|
||||
TextView textViewCreated = new TextView(this);
|
||||
textViewCreated.setText(userRegion.getCreated());
|
||||
textViewCreated.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
|
||||
row.addView(textViewRegionId);
|
||||
row.addView(textViewPlayCount);
|
||||
row.addView(textViewProvince);
|
||||
row.addView(textViewCreated);
|
||||
tableLayout.addView(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,6 @@ public class MusicFragment extends Fragment {
|
||||
if (response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
MaiUser maiUser = new Gson().fromJson(json, MaiUser.class);
|
||||
saveMusicRatings(maiUser.getUserMusicList());
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
musicRatings.clear();
|
||||
for (UserMusicList musicSongsRating : maiUser.getUserMusicList()) {
|
||||
|
||||
@@ -14,15 +14,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:id="@+id/userBox"
|
||||
android:visibility="gone"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/userId"
|
||||
android:id="@+id/key"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="qqbot"
|
||||
android:hint="Union绑定uuid"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:clickable="false"
|
||||
@@ -31,45 +29,21 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/key"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="用户邮箱"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/safecode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="2fa应用内的6位数字代码(注册不用填)"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton android:layout_width="match_parent"
|
||||
android:text="登录/注册 账号"
|
||||
android:text="绑定"
|
||||
android:textColor="@android:color/white"
|
||||
|
||||
android:id="@+id/bangding"
|
||||
android:layout_height="wrap_content">
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/getTicket"
|
||||
android:id="@+id/test"
|
||||
android:textColor="@android:color/white"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="拿 6 倍卷(公众号先发码)"/>
|
||||
android:text="ping"/>
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/tableLayout"
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<item
|
||||
android:id="@+id/nav_music"
|
||||
android:title="@string/menu_music"/>
|
||||
<item
|
||||
android:id="@+id/nav_pixiv"
|
||||
android:title="@string/menu_pixiv"/>
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/nav_pixiv"-->
|
||||
<!-- android:title="@string/menu_pixiv"/>-->
|
||||
<item
|
||||
android:id="@+id/nav_slideshow"
|
||||
android:title="@string/menu_slideshow"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.0' apply false
|
||||
id 'com.android.application' version '8.8.0' apply false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user