diff --git a/config.json b/config.json index 0bff65d..bb2f5d6 100644 --- a/config.json +++ b/config.json @@ -18,8 +18,17 @@ "to": "#ffccff00" } }, - "cleanup": { - "keep_dirs": ["META-INF", "kotlin"] + "compress": { + "remove_language_files": true, + "remove_AI_voiceover": true, + "remove_debug_lines": true, + "remove_drawable_files": false, + "remove_unknown_files": true, + "remove_unknown_files_keep_dirs": [ + "META-INF", + "kotlin" + ], + "compress_png_files": true }, "settings_urls": { "Мы в социальных сетях": [ @@ -59,5 +68,7 @@ "android": "http://schemas.android.com/apk/res/android", "app": "http://schemas.android.com/apk/res-auto" }, - "speeds": [9.0] -} + "speeds": [ + 9.0 + ] +} \ No newline at end of file diff --git a/patches/cleanup.py b/patches/cleanup.py deleted file mode 100644 index 7de37e5..0000000 --- a/patches/cleanup.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Remove unnecessary files""" -priority = 0 -from tqdm import tqdm - -import os -import shutil - - -def apply(config: dict) -> bool: - for item in os.listdir("./decompiled/unknown/"): - item_path = os.path.join("./decompiled/unknown/", item) - - if os.path.isfile(item_path): - os.remove(item_path) - if config.get("verbose", False): - tqdm.write(f'Удалён файл: {item_path}') - elif os.path.isdir(item_path): - if item not in config["cleanup"]["keep_dirs"]: - shutil.rmtree(item_path) - if config.get("verbose", False): - tqdm.write(f'Удалена папка: {item_path}') - return True diff --git a/patches/compress.md b/patches/compress.md new file mode 100644 index 0000000..ca2bf88 --- /dev/null +++ b/patches/compress.md @@ -0,0 +1,30 @@ +# Compress + +Патч удаляет ненужные ресурсы что-бы уменьшить размер АПК + +## настройки (compress в config.json) + +- remove_unknown_files: true/false - удаляет файлы из директории decompiled/unknown +- remove_unknown_files_keep_dirs: list[str] - оставляет указанные директории в decompiled/unknown +- remove_debug_lines: true/false - удаляет строки `.line n` из декомпилированных smali файлов использованные для дебага +- remove_AI_voiceover: true/false - заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами +- compress_png_files: true/false - сжимает PNG в директории decompiled/res +- remove_drawable_files: true/false - удаляет неиспользованные drawable-* из директории decompiled/res +- remove_language_files: true/false - удаляет все языки кроме русского и английского + +## efficiency + +Проверено с версией 9.0 Beta 7 + +разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи + +| Настройка | Размер файла | Разница | % | +| :----------- | :-------------------: | :-----------------: | :-: | +| None | 17092 bytes - 17.1 MB | - | - | +| Compress PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% | +| Remove files | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% | +| Remove draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% | +| Remove lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% | +| Remove ai vo | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% | +| Remove langs | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% | +| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% | diff --git a/patches/compress.py b/patches/compress.py new file mode 100644 index 0000000..146e377 --- /dev/null +++ b/patches/compress.py @@ -0,0 +1,270 @@ +"""Remove and compress resources""" + +priority = -1 + +# imports +import os +import shutil +import subprocess +from tqdm import tqdm +from utils.smali_parser import get_smali_lines, save_smali_lines + + +# Patch +def remove_unknown_files(config): + path = "./decompiled/unknown" + items = os.listdir(path) + for item in items: + item_path = f"{path}/{item}" + if os.path.isfile(item_path): + os.remove(item_path) + if config.get("verbose", False): + tqdm.write(f"Удалён файл: {item_path}") + elif os.path.isdir(item_path): + if item not in config['compress']["remove_unknown_files_keep_dirs"]: + shutil.rmtree(item_path) + if config.get("verbose", False): + tqdm.write(f"Удалёна директория: {item_path}") + return True + + +def remove_debug_lines(config): + for root, _, files in os.walk("./decompiled"): + for filename in files: + file_path = os.path.join(root, filename) + if os.path.isfile(file_path) and filename.endswith(".smali"): + file_content = get_smali_lines(file_path) + new_content = [] + for line in file_content: + if line.find(".line") >= 0: + continue + new_content.append(line) + save_smali_lines(file_path, new_content) + if config.get("verbose", False): + tqdm.write(f"Удалены дебаг линии из: {file_path}") + return True + + +def compress_png(config, png_path: str): + try: + subprocess.run( + [ + "pngquant", + "--force", + "--ext", + ".png", + "--quality=65-90", + png_path, + ], + check=True, + capture_output=True, + ) + if config.get("verbose", False): + tqdm.write(f"Сжат файл PNG: {png_path}") + return True + except subprocess.CalledProcessError as e: + tqdm.write(f"Ошибка при сжатии {png_path}: {e}") + return False + + +def compress_png_files(config): + compressed = [] + for root, _, files in os.walk("./decompiled"): + for file in files: + if file.lower().endswith(".png"): + compress_png(config, f"{root}/{file}") + compressed.append(f"{root}/{file}") + return len(compressed) > 0 and any(compressed) + + +def remove_AI_voiceover(config): + blank = "./patches/resources/blank.mp3" + path = "./decompiled/res/raw" + files = [ + "reputation_1.mp3", + "reputation_2.mp3", + "reputation_3.mp3", + "sound_beta_1.mp3", + "sound_create_blog_1.mp3", + "sound_create_blog_2.mp3", + "sound_create_blog_3.mp3", + "sound_create_blog_4.mp3", + "sound_create_blog_5.mp3", + "sound_create_blog_6.mp3", + "sound_create_blog_reputation_1.mp3", + "sound_create_blog_reputation_2.mp3", + "sound_create_blog_reputation_3.mp3", + "sound_create_blog_reputation_4.mp3", + "sound_create_blog_reputation_5.mp3", + "sound_create_blog_reputation_6.mp3", + ] + + for file in files: + if os.path.exists(f"{path}/{file}"): + os.remove(f"{path}/{file}") + shutil.copyfile(blank, f"{path}/{file}") + if config.get("verbose", False): + tqdm.write(f"Файл mp3 был заменён на пустой: {path}/{file}") + + return True + +def remove_language_files(config): + path = "./patches/res" + folders = [ + "values-af", + "values-am", + "values-ar", + "values-as", + "values-az", + "values-b+es+419", + "values-b+sr+Latn", + "values-be", + "values-bg", + "values-bn", + "values-bs", + "values-ca", + "values-cs", + "values-da", + "values-de", + "values-el", + "values-en-rAU", + "values-en-rCA", + "values-en-rGB", + "values-en-rIN", + "values-en-rXC", + "values-es", + "values-es-rGT", + "values-es-rUS", + "values-et", + "values-eu", + "values-fa", + "values-fi", + "values-fr", + "values-fr-rCA", + "values-gl", + "values-gu", + "values-hi", + "values-hr", + "values-hu", + "values-hy", + "values-in", + "values-is", + "values-it", + "values-iw", + "values-ja", + "values-ka", + "values-kk", + "values-km", + "values-kn", + "values-ko", + "values-ky", + "values-lo", + "values-lt", + "values-lv", + "values-mk", + "values-ml", + "values-mn", + "values-mr", + "values-ms", + "values-my", + "values-nb", + "values-ne", + "values-nl", + "values-or", + "values-pa", + "values-pl", + "values-pt", + "values-pt-rBR", + "values-pt-rPT", + "values-ro", + "values-si", + "values-sk", + "values-sl", + "values-sq", + "values-sr", + "values-sv", + "values-sw", + "values-ta", + "values-te", + "values-th", + "values-tl", + "values-tr", + "values-uk", + "values-ur", + "values-uz", + "values-vi", + "values-zh", + "values-zh-rCN", + "values-zh-rHK", + "values-zh-rTW", + "values-zu", + "values-watch", + ] + + for folder in folders: + if os.path.exists(f"{path}/{folder}"): + shutil.rmtree(f"{path}/{folder}") + if config.get("verbose", False): + tqdm.write(f"Удалена директория: {path}/{folder}") + return True + + +def remove_drawable_files(config): + path = "./patches/res" + folders = [ + "drawable-en-hdpi", + "drawable-en-ldpi", + "drawable-en-mdpi", + "drawable-en-xhdpi", + "drawable-en-xxhdpi", + "drawable-en-xxxhdpi", + "drawable-ldrtl-hdpi", + "drawable-ldrtl-mdpi", + "drawable-ldrtl-xhdpi", + "drawable-ldrtl-xxhdpi", + "drawable-ldrtl-xxxhdpi", + "drawable-tr-anydpi", + "drawable-tr-hdpi", + "drawable-tr-ldpi", + "drawable-tr-mdpi", + "drawable-tr-xhdpi", + "drawable-tr-xxhdpi", + "drawable-tr-xxxhdpi", + "drawable-watch", + "layout-watch", + ] + + for folder in folders: + if os.path.exists(f"{path}/{folder}"): + shutil.rmtree(f"{path}/{folder}") + if config.get("verbose", False): + tqdm.write(f"Удалена директория: {path}/{folder}") + return True + + +def apply(config) -> bool: + if config['compress']["remove_unknown_files"]: + tqdm.write(f"Удаление неизвестных файлов...") + remove_unknown_files(config) + + if config['compress']["remove_drawable_files"]: + tqdm.write(f"Удаление директорий drawable-xx...") + remove_drawable_files(config) + + if config['compress']["compress_png_files"]: + tqdm.write(f"Сжатие PNG файлов...") + compress_png_files(config) + + if config['compress']["remove_language_files"]: + tqdm.write(f"Удаление языков...") + remove_language_files(config) + + if config['compress']["remove_AI_voiceover"]: + tqdm.write(f"Удаление ИИ озвучки...") + remove_AI_voiceover(config) + + if config['compress']["remove_debug_lines"]: + tqdm.write(f"Удаление дебаг линий...") + remove_debug_lines(config) + + return True diff --git a/patches/compress_png.py b/patches/compress_png.py deleted file mode 100644 index d71b3ab..0000000 --- a/patches/compress_png.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Compress PNGs""" -priority = -1 -from tqdm import tqdm - -import os -import subprocess - - -def compress_pngs(root_dir: str, verbose: bool = False): - compressed_files = [] - for dirpath, _, filenames in os.walk(root_dir): - for filename in filenames: - if filename.lower().endswith(".png"): - filepath = os.path.join(dirpath, filename) - if verbose: tqdm.write(f"Сжимаю: {filepath}") - try: - assert subprocess.run( - [ - "pngquant", - "--force", - "--ext", - ".png", - "--quality=65-90", - filepath, - ], - capture_output=True, - ).returncode in [0, 99] - compressed_files.append(filepath) - except subprocess.CalledProcessError as e: - tqdm.write(f"Ошибка при сжатии {filepath}: {e}") - return compressed_files - - -def apply(config: dict) -> bool: - files = compress_pngs("./decompiled", config.get("verbose", False)) - return len(files) > 0 and any(files) diff --git a/patches/resources/blank.mp3 b/patches/resources/blank.mp3 new file mode 100644 index 0000000..bfab560 Binary files /dev/null and b/patches/resources/blank.mp3 differ diff --git a/utils/smali_parser.py b/utils/smali_parser.py index f8d1c62..c2b8a78 100644 --- a/utils/smali_parser.py +++ b/utils/smali_parser.py @@ -4,24 +4,35 @@ def get_smali_lines(file: str) -> list[str]: lines = smali.readlines() return lines + +def save_smali_lines(file: str, lines: list[str]) -> None: + with open(file, "w", encoding="utf-8") as f: + f.writelines(lines) + + def find_smali_method_start(lines: list[str], index: int) -> int: while True: index -= 1 if lines[index].find(".method") >= 0: return index + def find_smali_method_end(lines: list[str], index: int) -> int: while True: index += 1 if lines[index].find(".end method") >= 0: return index + def debug_print_smali_method(lines: list[str], start: int, end: int) -> None: while start != (end + 1): print(start, lines[start]) start += 1 -def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines: list[str]) -> list[str]: + +def replace_smali_method_body( + lines: list[str], start: int, end: int, new_lines: list[str] +) -> list[str]: new_content = [] index = 0 skip = end - start - 1 @@ -29,30 +40,21 @@ def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines: while index != (start + 1): new_content.append(lines[index]) index += 1 - + for line in new_lines: new_content.append(line) - + index += skip while index < len(lines): new_content.append(lines[index]) index += 1 - return new_content -# example i guess -# if __name__ == "__main__": -# lines = get_smali_lines("./decompiled/smali_classes2/com/radiquum/anixart/Prefs.smali") - -# for index, line in enumerate(lines): -# if line.find("IS_SPONSOR") >= 0: -# method_start = find_smali_method_start(lines, index) -# method_end = find_smali_method_end(lines, index) -# new_content = replace_smali_method_body(lines, method_start, method_end, c) - -# with open("./help/Prefs_orig.smali", "w", encoding="utf-8") as file: -# file.writelines(lines) -# with open("./help/Prefs_modified.smali", "w", encoding="utf-8") as file: -# file.writelines(new_content) - \ No newline at end of file +def find_and_replace_smali_line( + lines: list[str], search: str, replace: str +) -> list[str]: + for index, line in enumerate(lines): + if line.find(search) >= 0: + lines[index] = lines[index].replace(search, replace) + return lines