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