├── 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 |
--------------------------------------------------------------------------------