diff --git a/.idea/FindMaimaiDX_Ultra.iml b/.idea/FindMaimaiDX_Ultra.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/FindMaimaiDX_Ultra.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56..b86273d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0ec0e09..d403a80 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,9 +1,10 @@
+
-
+
diff --git a/app/src/main/java/org/astral/findmaimaiultra/been/faker/MaimaiConfig.java b/app/src/main/java/org/astral/findmaimaiultra/been/faker/MaimaiConfig.java
new file mode 100644
index 0000000..1270741
--- /dev/null
+++ b/app/src/main/java/org/astral/findmaimaiultra/been/faker/MaimaiConfig.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/org/astral/findmaimaiultra/been/faker/SegaApi2025.java b/app/src/main/java/org/astral/findmaimaiultra/been/faker/SegaApi2025.java
new file mode 100644
index 0000000..a592d0b
--- /dev/null
+++ b/app/src/main/java/org/astral/findmaimaiultra/been/faker/SegaApi2025.java
@@ -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());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/astral/findmaimaiultra/service/InMemoryJarLoader.java b/app/src/main/java/org/astral/findmaimaiultra/service/InMemoryJarLoader.java
new file mode 100644
index 0000000..60cb9ee
--- /dev/null
+++ b/app/src/main/java/org/astral/findmaimaiultra/service/InMemoryJarLoader.java
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/astral/findmaimaiultra/ui/MainActivity.java b/app/src/main/java/org/astral/findmaimaiultra/ui/MainActivity.java
index 099ce8b..4ca5b5c 100644
--- a/app/src/main/java/org/astral/findmaimaiultra/ui/MainActivity.java
+++ b/app/src/main/java/org/astral/findmaimaiultra/ui/MainActivity.java
@@ -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);
diff --git a/app/src/main/java/org/astral/findmaimaiultra/ui/home/HomeFragment.java b/app/src/main/java/org/astral/findmaimaiultra/ui/home/HomeFragment.java
index eeb1c9d..de96909 100644
--- a/app/src/main/java/org/astral/findmaimaiultra/ui/home/HomeFragment.java
+++ b/app/src/main/java/org/astral/findmaimaiultra/ui/home/HomeFragment.java
@@ -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()));
diff --git a/app/src/main/java/org/astral/findmaimaiultra/ui/login/LinkQQBot.java b/app/src/main/java/org/astral/findmaimaiultra/ui/login/LinkQQBot.java
index 10a1de1..225c93c 100644
--- a/app/src/main/java/org/astral/findmaimaiultra/ui/login/LinkQQBot.java
+++ b/app/src/main/java/org/astral/findmaimaiultra/ui/login/LinkQQBot.java
@@ -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 userRegions) {
- Collections.sort(userRegions, new Comparator() {
- @Override
- public int compare(UserRegion o1, UserRegion o2) {
- return Integer.compare(o2.getPlayCount(), o1.getPlayCount());
- }
- });
- // 处理排序后的数据,例如显示在表格中
- displaySortedUserRegions(userRegions);
- }
-
- private void displaySortedUserRegions(List 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);
- }
}
}
diff --git a/app/src/main/java/org/astral/findmaimaiultra/ui/music/MusicFragment.java b/app/src/main/java/org/astral/findmaimaiultra/ui/music/MusicFragment.java
index c60ff89..173a978 100644
--- a/app/src/main/java/org/astral/findmaimaiultra/ui/music/MusicFragment.java
+++ b/app/src/main/java/org/astral/findmaimaiultra/ui/music/MusicFragment.java
@@ -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()) {
diff --git a/app/src/main/res/layout/activity_hack_get_user_id.xml b/app/src/main/res/layout/activity_hack_get_user_id.xml
index 9af0f01..74bc37b 100644
--- a/app/src/main/res/layout/activity_hack_get_user_id.xml
+++ b/app/src/main/res/layout/activity_hack_get_user_id.xml
@@ -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">
-
-
-
-
-
-
-
-
+ android:text="ping"/>
-
+
+
+
diff --git a/build.gradle b/build.gradle
index 8406cb7..91dc460 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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
}
diff --git a/gradle.properties b/gradle.properties
index 3e927b1..a03b354 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,4 +18,4 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true