├── 0-Setup.bat ├── 2-Patch.bat ├── 3-InstallAPK.bat ├── 1-BackupAppData.bat ├── 4-RestoreAppData.bat ├── tools ├── sign-key.jks └── _manifest ├── .gitignore ├── index.ts ├── package.json ├── LICENSE ├── README.md └── kmb-patch.ts /0-Setup.bat: -------------------------------------------------------------------------------- 1 | cmd /c npm install 2 | pause -------------------------------------------------------------------------------- /2-Patch.bat: -------------------------------------------------------------------------------- 1 | cmd /c npm run patch 2 | pause -------------------------------------------------------------------------------- /3-InstallAPK.bat: -------------------------------------------------------------------------------- 1 | cmd /c npm run install-apk 2 | pause -------------------------------------------------------------------------------- /1-BackupAppData.bat: -------------------------------------------------------------------------------- 1 | cmd /c npm run backup-app-data 2 | pause -------------------------------------------------------------------------------- /4-RestoreAppData.bat: -------------------------------------------------------------------------------- 1 | cmd /c npm run restore-app-data 2 | pause -------------------------------------------------------------------------------- /tools/sign-key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louislam/kmb-patch/HEAD/tools/sign-key.jks -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /.idea 3 | /node_modules 4 | 5 | /tools/* 6 | !/tools/sign-key.jks 7 | !/tools/_manifest 8 | 9 | /tmp 10 | 11 | kmb.apk 12 | patched-kmb.apk 13 | *.ab 14 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { KMBPatch } from "./kmb-patch"; 2 | 3 | let action = "patch"; 4 | 5 | if (process.argv.length == 3) { 6 | action = "restore" 7 | } 8 | 9 | (async () => { 10 | let remover = new KMBPatch("kmb.apk"); 11 | let exitCode = 0; 12 | 13 | if (action == "patch") { 14 | exitCode = await remover.patch(); 15 | } else if (action == "restore") { 16 | exitCode = await remover.restore(); 17 | } else { 18 | console.error("Incorrect action"); 19 | exitCode = 1; 20 | } 21 | 22 | process.exit(exitCode); 23 | })(); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kmb-patch", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "patch": "ts-node index.ts", 8 | "install-apk": "adb install patched-kmb.apk", 9 | "backup-app-data": "adb backup -f appdata.ab com.kmb.app1933", 10 | "restore-app-data": "ts-node index.ts restore" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@types/node": "^14.10.1", 16 | "axios": "^0.21.4", 17 | "cheerio": "^1.0.0-rc.3", 18 | "extract-zip": "^2.0.1", 19 | "glob": "^7.1.7", 20 | "progress": "^2.0.3", 21 | "tar-stream": "^2.2.0", 22 | "tempy": "^0.6.0", 23 | "ts-node": "~9.1.1", 24 | "typescript": "~4.0.8" 25 | }, 26 | "devDependencies": {} 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 LouisLam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/_manifest: -------------------------------------------------------------------------------- 1 | 1 2 | com.kmb.app1933 3 | 138 4 | 28 5 | 6 | 0 7 | 1 8 | 308203653082024da00302010202041b3930a2300d06092a864886f70d01010b05003062310b300906035504061302484b3112301006035504071309486f6e67204b6f6e6731153013060355040a130c4c6f7569736c616d2e6e657431153013060355040b130c4c6f7569736c616d2e6e65743111300f060355040313084c6f7569734c616d3020170d3230303931343133303834395a180f32303730303930323133303834395a3062310b300906035504061302484b3112301006035504071309486f6e67204b6f6e6731153013060355040a130c4c6f7569736c616d2e6e657431153013060355040b130c4c6f7569736c616d2e6e65743111300f060355040313084c6f7569734c616d30820122300d06092a864886f70d01010105000382010f003082010a0282010100a1e585cae6803d31d520226aee1f85b488206fb4808471a8f6b535835c71b8dd270f47b5e54adaeaf3728b579e9325fb04582f767d8cdc3622b010ec1968dcff780ea479264413d0af334883edc5b732ad942855c4fcd1ed297298e3b950cad3ff1829ecda800e29a114f6db6fcde00a2b01942f9f0b6cff127a0082893ee8c5877e567bf88e7332ac7b754483798dae7f87de2809d13f45a30a94128bc8c048e19d81b7047fcd72fa1fe35fc38047de659caeb6ca5842aa789aa18325227d0948df3719fc0102a50e556884ba57187143cfa4b4390727e2ea5294da8a5c097213ac99ef1e49de941b66bac2555ca08305a15410cfc554e4163d709d519753f10203010001a321301f301d0603551d0e041604147584ae074fcb9150851e68deac492f31ce610fba300d06092a864886f70d01010b05000382010100037282c6ecbda4e17cbcb58251631a9db9fe179d904cd6aa6cf02e699d9882967b12198e9eb001484f4afb9b440ced04c037e902aab296db7484e51e410e0330b4e5a762451a5c6bdf1006af213d373c4428c9cbc966dc1786a2a894eafdd4d59a6037abab6cf785f7933ce41e16ecb98cbb074257c37d32e381b52a876762a0aba14e7da378e641e0b4cdb7ab43669344711e88a7fb34aa7f224d8974ba4cfac5b304b797348ca86178577546a0c16870cecadafec7c22db0d8c48e5d3b08bcc77eb83c2a8eb89e38ebf1f04db8c30a60e1878b741e7b994930500c0d3f331f56d39d626454df08f62b447e7c9897a2f97126d81cd6fdcd3497c84792a41dea 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KMB Patch for Android APK 2 | 3 | (2025-07-26) Update: 舊版本的 KMB APP 已無法正常使用 (無法更新路線資料)。而目前 2.3.23 亦都無法 Patch。因為隻 APP 已經重新寫過,並且係 React Native。由於目前 Disassembe/Assemble React Native 並不成熟,要 Patch 嘅難度大增。加上目前新版好似冇以前咁慢,廣告亦都冇以前咁痴線。因此呢個項目係時候完,謝謝支持。 4 | 5 | ## Description 6 | 7 | 已測試版本: 8 | * 2.0.5 可用 (2023-08-02) 9 | * 1.9.2 可用 10 | * 1.8.2 可用 11 | * 1.7.9 可用 12 | * ~~1.6.8 可用~~ (1.7.8 及更舊版本無法獲取路線資料更新) 13 | * ~~1.6.6 可用~~ 14 | 15 | 1. 略過 Splash Screen,快速啟動 16 | 2. 移除廣告 17 | 3. Open Source 自助修補,APK 唔怕俾壞人加料 18 | 4. 可備份原版 App Data 19 | 20 | ## Demo 21 | 22 | 影片示範,使用 KMB Patch 後,幾咁快同埋幾乾淨。 23 | [(點擊播放)](https://youtu.be/hwvs_Z5rMbo) 24 | 25 | [](https://youtu.be/hwvs_Z5rMbo) 26 | 27 | ## Requirements 28 | 29 | ### Windows (64bit) 30 | 31 | * [Node.js](https://nodejs.org/dist/v12.18.3/node-v12.18.3-x64.msi) 12.0 或以上 32 | * [ADB](https://dl.google.com/android/repository/platform-tools-latest-windows.zip) (如不需要備份 App Data,可不用) 33 | 34 | ### Linux 35 | 36 | 可生成APK,但未知可否用備份功能。 37 | 38 | * 已安裝 Java 39 | * 已安裝 Node.js 12 或以上 40 | 41 | (已測試 Ubuntu 19.10) 42 | 43 | ## How to use 44 | 45 | 1. 安裝 [Node.js](https://nodejs.org/en/download) 46 | 2. 下載這個程式 https://github.com/louislam/kmb-patch/archive/2.0.0.zip ,並解壓縮。 47 | 3. 進入資料夾,執行 `0-Setup.bat` 或 `npm install` 。 48 | 4. (非必要 ADB) 如需要備份 Bookmark 等資料,可先把 Android 手機連接到 PC,再執行 `1-BackupAppData.bat` 。 49 | 5. 匯出原版 APK,或到可信的網站下載,例如 https://apkpure.com/app-1933-kmb-lwb/com.kmb.app1933 ,把檔案命名為 `kmb.apk`,然後放到同一資料夾下。 50 | 6. 執行 `2-Patch.bat`,成功後,會生成 `patched-kmb.apk` 。 51 | 7. 喺你部 Android 機刪除原版 KMB App 。 52 | 8. 用你鍾意嘅方法,將 `patched-kmb.apk` 放入你部 Android 機,然後安裝。 53 | 9. (非必要 ADB) 如要恢復備份,可執行 `4-RestoreAppData.bat`。 54 | 55 | ### TLDR? 56 | 57 | ``` 58 | kmb.apk + 2-Patch.bat => patched-kmb.apk 59 | ``` 60 | 61 | ## Additional 62 | 63 | * 由於 apk 已由另一條 Key 重新簽署,所以 Google Map API Key 都要同時換先用到。 64 | * 有興趣了解更多設定或修補過程,可以打開 kmb-patch.ts 研究研究。 65 | 66 | -------------------------------------------------------------------------------- /kmb-patch.ts: -------------------------------------------------------------------------------- 1 | const tempy = require('tempy'); 2 | const {execSync} = require('child_process'); 3 | const os = require('os'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const axios = require('axios'); 7 | const ProgressBar = require('progress'); 8 | const extract = require("extract-zip"); 9 | const cheerio = require('cheerio'); 10 | const glob = require("glob"); 11 | const tar = require('tar-stream'); 12 | 13 | /** 14 | * KMB Patch Class 15 | */ 16 | export class KMBPatch { 17 | public inputAPK : string; 18 | public outputAPK : string; 19 | public tempDir : string; 20 | public forceOverwrite = true; 21 | public mapKey = ""; 22 | 23 | public signKey = "tools/sign-key.jks"; 24 | public keystorePassword = ""; 25 | public keyAlias = "louislam"; 26 | public keyPassword = ""; 27 | 28 | public downloadJava = true; 29 | public java = '"tools/jdk-11.0.8+10-jre/bin/java"'; 30 | 31 | 32 | constructor(inputAPK : string, outputAPK = "patched-kmb.apk", tempDir = "tmp") { 33 | this.mapKey = Buffer.from("QUl6YVN5QXR6Y2t0UzFfb1RBOHJ5dXBXdjFqcENCUXJZRjNHVVJr", 'base64').toString('utf8'); 34 | this.keystorePassword = Buffer.from("Q25oUWJuZ3U4YUxGVlI2ckhHczZ6a29yeVpjSlZlREY=", 'base64').toString('utf8'); 35 | this.keyPassword = this.keystorePassword; 36 | 37 | this.inputAPK = inputAPK; 38 | this.outputAPK = outputAPK; 39 | 40 | if (tempDir == null) { 41 | this.tempDir = tempy.directory(); 42 | this.forceOverwrite = true; 43 | } else { 44 | this.tempDir = tempDir; 45 | } 46 | 47 | console.log("Temp Dir: " + this.tempDir); 48 | console.log("OS: " + os.platform()); 49 | 50 | if (os.platform() !== "win32") { 51 | this.downloadJava = false; 52 | this.java = "java"; 53 | } 54 | } 55 | 56 | /** 57 | * Patch the apk 58 | */ 59 | async patch() { 60 | let exitCode = 0; 61 | let self = this; 62 | 63 | try { 64 | await this.downloadTools(); 65 | 66 | if (fs.existsSync(this.tempDir)){ 67 | fs.rmSync(this.tempDir, { 68 | recursive: true 69 | }); 70 | } 71 | 72 | let escapedTempDir = escapeShellArg(this.tempDir); 73 | let escapedInputAPK = escapeShellArg(this.inputAPK); 74 | let escapedOutputAPK; 75 | let f = ""; 76 | 77 | let escapedSignKey = escapeShellArg(this.signKey); 78 | let escapedKeystonePassword = escapeShellArg(this.keystorePassword); 79 | let escapedKeyAlias = escapeShellArg(this.keyAlias); 80 | let escapedKeyPassword = escapeShellArg(this.keyPassword); 81 | 82 | if (this.forceOverwrite) { 83 | f = "-f"; 84 | } 85 | 86 | console.log("Extracting APK"); 87 | let output : string = execSync(`${this.java} -Xmx512m -jar tools/apktool_2.4.1.jar d -o ${escapedTempDir} ${f} ${escapedInputAPK}`).toString(); 88 | console.log(output); 89 | 90 | // Patch AndroidManifest.xml 91 | console.log("Patch AndroidManifest.xml"); 92 | let xmlPath = this.tempDir + "/AndroidManifest.xml"; 93 | let androidManifestXML : string = fs.readFileSync(xmlPath); 94 | let $ = cheerio.load(androidManifestXML, { 95 | xmlMode: true 96 | }); 97 | 98 | escapedOutputAPK = escapeShellArg(this.outputAPK); 99 | 100 | // Update MAP Key 101 | $("application meta-data").each(function () { 102 | if ($(this).attr("android:name") == "com.google.android.maps.v2.API_KEY") { 103 | $(this).attr("android:value", self.mapKey); 104 | } 105 | }); 106 | 107 | // Remove Splash Screen 108 | console.log("Remove Splash Screen"); 109 | $("activity").each(function () { 110 | if ($(this).attr("android:name") == "com.mobilesoft.mybus.KMBMainView") { 111 | $(this).append(` 112 | 113 | 114 | 115 | 116 | `); 117 | } else if ($(this).attr("android:name") == "com.mobilesoft.mybus.KMBSplashScreen") { 118 | $(this).empty(); 119 | } 120 | }); 121 | 122 | fs.writeFileSync(xmlPath, $.xml()); 123 | 124 | // Remove AdView in xml 125 | let resXMLPath = this.tempDir + '/res/**/*.xml'; 126 | let fileList = glob.sync(resXMLPath); 127 | 128 | for (let i = 0; i < fileList.length; i++) { 129 | let filename = fileList[i]; 130 | let text = fs.readFileSync(filename).toString(); 131 | 132 | if (text.includes("com.google.android.gms.ads.AdView")) { 133 | console.log("Remove AdView in " + filename); 134 | $ = cheerio.load(text, { 135 | xmlMode: true 136 | }); 137 | $("com\\.google\\.android\\.gms\\.ads\\.AdView").remove(); 138 | fs.writeFileSync(filename, $.xml()); 139 | } 140 | } 141 | 142 | // smali_classes2 folder 143 | let path = this.tempDir + '/smali_classes2/**/*.smali'; 144 | fileList = glob.sync(path); 145 | 146 | for (let i = 0; i < fileList.length; i++) { 147 | let updated = false; 148 | let filename = fileList[i]; 149 | let text = fs.readFileSync(filename).toString(); 150 | let lines = text.split(/\r?\n/); 151 | 152 | for (let j = 0; j < lines.length; j++) { 153 | let line = lines[j]; 154 | 155 | // Disable check update 156 | if (line.includes(", 0x2712")) { 157 | console.log("Remove force update in " + filename); 158 | lines[j] = line.replace(", 0x2712", ", 0x2760"); 159 | updated = true; 160 | } 161 | 162 | // Remove loadAd code 163 | if (line.includes("->loadAd(")) { 164 | console.log("Remove loadAd code in " + filename); 165 | lines[j] = "#" + line; 166 | 167 | // Also comment previous line if is .line 168 | if (lines[j - 1].trim().startsWith(".line")) { 169 | lines[j - 1] = "#" + lines[j - 1]; 170 | } 171 | 172 | updated = true; 173 | } 174 | 175 | // Remove setVisibility code 176 | if (line.includes("AdView;->setVisibility")) { 177 | console.log("Remove setVisibility code in " + filename); 178 | lines[j] = "#" + line; 179 | 180 | // Also comment previous line if is .line 181 | if (lines[j - 1].trim().startsWith(".line")) { 182 | lines[j - 1] = "#" + lines[j - 1]; 183 | } 184 | 185 | updated = true; 186 | } 187 | 188 | // Remove InterstitialAd 189 | // Smali: InterstitialAd;->load ==== JAVA: InterstitialAd.load(...) 190 | if (line.includes("InterstitialAd;->load")) { 191 | console.log("Remove InterstitialAd code in " + filename); 192 | lines[j] = "#" + line; 193 | 194 | // Also comment previous line if is .line 195 | if (lines[j - 1].trim().startsWith(".line")) { 196 | lines[j - 1] = "#" + lines[j - 1]; 197 | } 198 | 199 | updated = true; 200 | } 201 | 202 | 203 | } 204 | 205 | if (updated) { 206 | fs.writeFileSync(filename, lines.join(os.EOL)); 207 | } 208 | } 209 | 210 | // /smali/ folder 211 | path = this.tempDir + '/smali/**/*.smali'; 212 | fileList = glob.sync(path); 213 | 214 | for (let i = 0; i < fileList.length; i++) { 215 | let updated = false; 216 | let filename = fileList[i]; 217 | let text = fs.readFileSync(filename).toString(); 218 | let lines = text.split(/\r?\n/); 219 | 220 | for (let j = 0; j < lines.length; j++) { 221 | let line = lines[j]; 222 | 223 | // Remove Builtin Ads 224 | if (line.includes("https://app.kmb.hk/app1933/index.php")) { 225 | console.log("Remove Builtin Ads in " + filename); 226 | 227 | // Keep finding `/mybus/manager/m` (JAVA: mybus.manager.m(...)), if found, add # at the beginning to comment it 228 | let k = j; 229 | let foundTheCall = false; 230 | 231 | while (k >= 0 && k < lines.length) { 232 | if (lines[k].includes("/mybus/manager/m")) { 233 | lines[k] = "#" + lines[k]; 234 | foundTheCall = true; 235 | break; 236 | } 237 | k++; 238 | } 239 | 240 | if (!foundTheCall) { 241 | console.error("Failed to remove Builtin Ads in " + filename); 242 | } else { 243 | updated = true; 244 | } 245 | } 246 | } 247 | 248 | if (updated) { 249 | fs.writeFileSync(filename, lines.join(os.EOL)); 250 | } 251 | } 252 | 253 | console.log("Build APK"); 254 | output = execSync(`${this.java} -Xmx512m -jar tools/apktool_2.4.1.jar b ${escapedTempDir} -o ${escapedOutputAPK}`).toString(); 255 | console.log(output); 256 | 257 | console.log("Sign the APK"); 258 | output = execSync(`${this.java} -Xmx512m -jar tools/uber-apk-signer-1.1.0.jar -a ${escapedOutputAPK} --allowResign --overwrite --ks ${escapedSignKey} --ksPass ${escapedKeystonePassword} --ksAlias ${escapedKeyAlias} --ksKeyPass ${escapedKeyPassword}`).toString(); 259 | console.log(output); 260 | 261 | console.log("Patched successfully! The patch apk file located in " + this.outputAPK); 262 | 263 | 264 | } catch (error) { 265 | console.error(error.message); 266 | exitCode = 1; 267 | } 268 | 269 | if (fs.existsSync(this.tempDir)){ 270 | fs.rmSync(this.tempDir, { 271 | recursive: true 272 | }); 273 | } 274 | 275 | return exitCode; 276 | } 277 | 278 | /** 279 | * Download all tools 280 | */ 281 | async downloadTools() { 282 | console.log("Download Tools"); 283 | 284 | if (! fs.existsSync("tools/apktool_2.4.1.jar")) { 285 | await this.downloadFile("https://github.com/iBotPeaches/Apktool/releases/download/v2.4.1/apktool_2.4.1.jar", "apktool_2.4.1.jar"); 286 | } 287 | 288 | if (! fs.existsSync("tools/uber-apk-signer-1.1.0.jar")) { 289 | await this.downloadFile("https://github.com/patrickfav/uber-apk-signer/releases/download/v1.1.0/uber-apk-signer-1.1.0.jar", "uber-apk-signer-1.1.0.jar"); 290 | } 291 | 292 | if (! fs.existsSync("tools/abe.jar")) { 293 | await this.downloadFile("https://github.com/nelenkov/android-backup-extractor/releases/download/20181012025725-d750899/abe-all.jar", "abe.jar") 294 | } 295 | 296 | // JRE Download link from https://adoptopenjdk.net/archive.html 297 | if (this.downloadJava && ! fs.existsSync("tools/jdk-11.0.8+10-jre")) { 298 | await this.downloadFile("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jre_x64_windows_hotspot_11.0.8_10.zip", "jre.zip"); 299 | 300 | await extract("tools/jre.zip", { 301 | dir: path.resolve("tools") 302 | }); 303 | } 304 | console.log("Downloaded all tools"); 305 | } 306 | 307 | /** 308 | * Download a file 309 | * Copy from https://futurestud.io/tutorials/axios-download-progress-in-node-js 310 | */ 311 | async downloadFile(url, filename) { 312 | console.log('Download ' + path.basename(url)) 313 | const { data, headers } = await axios({ 314 | url, 315 | method: 'GET', 316 | responseType: 'stream' 317 | }); 318 | const totalLength = parseInt(headers['content-length']); 319 | 320 | const progressBar = new ProgressBar('downloading [:bar] :percent :etas', { 321 | width: 40, 322 | complete: '=', 323 | incomplete: ' ', 324 | renderThrottle: 1, 325 | total: totalLength 326 | }); 327 | 328 | const writer = fs.createWriteStream(path.resolve(__dirname, 'tools', filename)); 329 | 330 | data.on('data', (chunk) => { 331 | progressBar.tick(chunk.length); 332 | data.finished 333 | }); 334 | 335 | 336 | 337 | await new Promise((resolve) => { 338 | writer.on("finish", () => { 339 | console.log("Downloaded and renamed to " + filename); 340 | resolve(); 341 | }); 342 | 343 | data.pipe(writer) 344 | }); 345 | 346 | } 347 | 348 | /** 349 | * Restore and patch the appdata.ab 350 | */ 351 | async restore() { 352 | try { 353 | if (! fs.existsSync("tmp")) { 354 | fs.mkdirSync("tmp"); 355 | } 356 | 357 | if (! fs.existsSync("tmp/appdata")) { 358 | fs.mkdirSync("tmp/appdata"); 359 | } 360 | 361 | if (! fs.existsSync("appdata.ab")) { 362 | throw "appdata.ab not found"; 363 | } 364 | 365 | console.log("Patching backup") 366 | 367 | execSync(`${this.java} -jar tools/abe.jar unpack appdata.ab tmp/appdata.tar`); 368 | 369 | // Stream is fun 370 | // oldTarballStream -> extract (Stream) -> only replace "_manifest" -> pack (Stream) -> newTarballStream 371 | 372 | let oldTarballStream = fs.createReadStream("tmp/appdata.tar"); 373 | let newTarballStream = fs.createWriteStream("tmp/patched-appdata.tar"); 374 | 375 | var pack = tar.pack(); 376 | var extract = tar.extract(); 377 | 378 | extract.on('entry', function(header, stream, callback) { 379 | if (header.name == "apps/com.kmb.app1933/_manifest") { 380 | let stat = fs.statSync("tools/_manifest"); 381 | let manifestStream = fs.createReadStream("tools/_manifest"); 382 | header.size = stat.size; 383 | manifestStream.pipe(pack.entry(header, callback)) 384 | } else { 385 | stream.pipe(pack.entry(header, callback)) 386 | } 387 | }) 388 | 389 | extract.on('finish', function () { 390 | pack.finalize() 391 | }) 392 | 393 | await new Promise((resolve) => { 394 | newTarballStream.on("finish", function () { 395 | newTarballStream.end(); 396 | resolve(); 397 | }); 398 | 399 | oldTarballStream.pipe(extract); 400 | pack.pipe(newTarballStream); 401 | }); 402 | 403 | execSync(`${this.java} -jar tools/abe.jar pack tmp/patched-appdata.tar patched-appdata.ab`); 404 | 405 | console.log("Connect your phone to your PC and accept restore"); 406 | execSync(`adb restore patched-appdata.ab`); 407 | 408 | } catch (error) { 409 | console.error(error.message); 410 | return 1; 411 | } 412 | 413 | return 0; 414 | } 415 | } 416 | 417 | function escapeShellArg(arg) { 418 | let quote; 419 | 420 | if (os.platform() == 'win32') { 421 | quote = '"'; 422 | } else { 423 | quote = "'"; 424 | } 425 | 426 | return quote + `${arg.replace(/'/g, `'\\''`)}` + quote; 427 | } 428 | --------------------------------------------------------------------------------