Files
LihonT/tests/test_app_icons.py
cheb0chik ce1d870d54 LihonT 2 Luna beta: source release
Mod for TikTok 47.1.3 (arm64) in Material 3 style: bottom bar, profile menu, TikTok settings and
comments; downloads without the watermark; switches for ads, live streams and the Shop tab; badges of
other mods next to names and the niche mod badge (members service); app icons; mod settings screen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:32:13 +03:00

173 lines
8.9 KiB
Python

"""Launcher icon aliases and the dead drawables that carry their pictures."""
import struct
import tempfile
import unittest
import zlib
from pathlib import Path
from lihont import app_icons
from lihont.app_icons import Icon, IconError
from lihont.apkzip import Apk
from lihont.arsc import Arsc
from lihont.axml import Axml
from lihont.manifest import entry_icon, launcher_elements
ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "src" / "icons"
BASE = ROOT / "OriginalAPKtiktok" / (
"com.zhiliaoapp.musically_47.1.3-2024701030_minAPI23(arm64-v8a)(nodpi)_apkmirror.com.apk")
def png(width, height):
"""The smallest thing png_side will accept: signature and an IHDR chunk."""
header = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
chunk = b"IHDR" + header
return (app_icons.PNG_SIGNATURE + struct.pack(">I", len(header)) + chunk
+ struct.pack(">I", zlib.crc32(chunk) & 0xFFFFFFFF) + b"\0" * 8)
class CatalogTest(unittest.TestCase):
def test_the_shipped_catalog_is_valid(self):
icons = app_icons.load_catalog(CATALOG)
self.assertEqual(["tolstyj", "elmacho", "pushistik", "original"], [i.slug for i in icons])
self.assertEqual(["Эй толстый", "Эль мачо", "Пушистик", "Оригинал"], [i.title for i in icons])
for icon in icons:
self.assertEqual(256, app_icons.png_side(icon.png))
def test_pictures_must_be_small_square_pngs(self):
self.assertEqual(256, app_icons.png_side(png(256, 256)))
for bad in (b"", b"not a png at all, not at all, not at all!!", png(256, 200), png(64, 64), png(2048, 2048)):
with self.assertRaises(IconError):
app_icons.png_side(bad)
def test_catalog_errors_are_reported(self):
with tempfile.TemporaryDirectory() as folder:
directory = Path(folder)
(directory / "ok.png").write_bytes(png(256, 256))
for text in ("a\tonly two\n", "Bad Slug\tTitle\tok.png\n", "dup\tT\tok.png\ndup\tU\tok.png\n",
"fine\t\tok.png\n", "fine\t" + "x" * 33 + "\tok.png\n"):
(directory / "catalog.tsv").write_text(text, encoding="utf-8")
with self.assertRaises(IconError, msg=text):
app_icons.load_catalog(directory)
(directory / "catalog.tsv").write_text("# comment\n\nfine\tTitle\tok.png\n", encoding="utf-8")
self.assertEqual(["fine"], [i.slug for i in app_icons.load_catalog(directory)])
def test_new_path_keeps_the_encoded_length(self):
for _, old in app_icons.SLOTS:
for index in range(len(app_icons.SLOTS)):
new = app_icons.slot_path(old, index)
self.assertEqual(len(old), len(new))
self.assertEqual(old.rpartition("/")[0], new.rpartition("/")[0])
self.assertTrue(new.endswith(".png"))
self.assertEqual("res/c/li0.png", app_icons.slot_path("res/c/a0g.png", 0))
with self.assertRaises(IconError):
app_icons.slot_path("res/c/a.png", 0)
def test_slots_are_distinct(self):
self.assertEqual(len(app_icons.SLOTS), len({r for r, _ in app_icons.SLOTS}))
self.assertEqual(len(app_icons.SLOTS), len({p for _, p in app_icons.SLOTS}))
class OnTheRealApkTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
if not BASE.exists():
raise unittest.SkipTest("clean TikTok base APK is not available")
def setUp(self):
self.apk = Apk(str(BASE))
self.addCleanup(self.apk.close)
def test_every_pinned_slot_is_what_the_table_says(self):
arsc = Arsc(self.apk.read("resources.arsc"))
for res_id, path in app_icons.SLOTS:
app_icons.verify_slot(arsc, res_id, path)
self.assertTrue(self.apk.has(path), path)
def test_a_slot_that_moved_is_refused(self):
arsc = Arsc(self.apk.read("resources.arsc"))
with self.assertRaises(IconError):
app_icons.verify_slot(arsc, app_icons.SLOTS[0][0], "res/c/zzz.png")
with self.assertRaises(IconError):
app_icons.verify_slot(arsc, 0x7F010000, "res/c/a0g.png")
def test_aliases_are_added_disabled_and_carry_their_pictures(self):
xml = Axml.parse(self.apk.read("AndroidManifest.xml"))
before = len(launcher_elements(xml))
icons = app_icons.load_catalog(CATALOG)
report = app_icons.apply(self.apk, xml, icons)
self.assertEqual(4, report["icons"])
self.assertEqual(app_icons.ORIGINAL_ENTRY, report["original_entry"])
reparsed = Axml.parse(self.apk.read("AndroidManifest.xml"))
self.assertEqual(before + 4, len(launcher_elements(reparsed)))
aliases = {reparsed.attr_string(n, "name"): n for n in reparsed.elements("activity-alias")}
arsc = Arsc(self.apk.read("resources.arsc"))
for index, icon in enumerate(icons):
name = app_icons.alias_name(icon.slug)
node = aliases[name]
res_id, old_path = app_icons.SLOTS[index]
self.assertEqual(0, reparsed.attr(node, "enabled").data, "starts disabled")
self.assertEqual(0xFFFFFFFF, reparsed.attr(node, "exported").data)
self.assertEqual(report["target"], reparsed.attr_string(node, "targetActivity"))
self.assertEqual("TikTok", reparsed.attr_string(node, "label"))
self.assertEqual(res_id, reparsed.attr(node, "icon").data)
# the table now sends that id to our file, and the file is in the zip
new_path = app_icons.slot_path(old_path, index)
self.assertEqual(new_path, arsc.file_path(arsc.values(res_id)[0]))
self.assertEqual(icon.png, self.apk.read(new_path))
self.assertFalse(self.apk.has(old_path))
titles = [reparsed.attr_string(n, "value") for n in reparsed.elements("meta-data")
if reparsed.attr_string(n, "name") == "lihont.title"]
self.assertEqual([app_icons.STOCK_TITLE] + [i.title for i in icons], titles, "stock icon first, then the catalog")
def test_the_original_entry_stays_callable_but_leaves_the_home_screen(self):
xml = Axml.parse(self.apk.read("AndroidManifest.xml"))
original_icon = entry_icon(xml, app_icons.ORIGINAL_ENTRY)
app_icons.apply(self.apk, xml, app_icons.load_catalog(CATALOG))
reparsed = Axml.parse(self.apk.read("AndroidManifest.xml"))
original = next(n for n in reparsed.elements("activity-alias")
if reparsed.attr_string(n, "name") == app_icons.ORIGINAL_ENTRY)
enabled = reparsed.attr(original, "enabled")
self.assertTrue(enabled is None or enabled.data != 0, "TikTok starts it by name, so it must stay on")
launchers = [reparsed.attr_string(n, "name") for n in launcher_elements(reparsed)]
self.assertNotIn(app_icons.ORIGINAL_ENTRY, launchers)
self.assertEqual(5, len(launchers))
stock = next(n for n in reparsed.elements("activity-alias")
if reparsed.attr_string(n, "name") == app_icons.alias_name(app_icons.STOCK_SLUG))
self.assertEqual(0xFFFFFFFF, reparsed.attr(stock, "enabled").data, "the stock icon is on by default")
self.assertEqual(original_icon, reparsed.attr(stock, "icon").data, "and it is TikTok's own picture")
def test_exactly_one_icon_is_on(self):
xml = Axml.parse(self.apk.read("AndroidManifest.xml"))
app_icons.apply(self.apk, xml, app_icons.load_catalog(CATALOG))
reparsed = Axml.parse(self.apk.read("AndroidManifest.xml"))
on = [reparsed.attr_string(n, "name") for n in launcher_elements(reparsed)
if reparsed.attr(n, "enabled") is None or reparsed.attr(n, "enabled").data != 0]
self.assertEqual([app_icons.alias_name(app_icons.STOCK_SLUG)], on)
def test_a_catalog_icon_cannot_take_the_stock_slug(self):
with tempfile.TemporaryDirectory() as folder:
directory = Path(folder)
(directory / "p.png").write_bytes(png(256, 256))
(directory / "catalog.tsv").write_text("stock\tTikTok\tp.png\n", encoding="utf-8")
with self.assertRaises(IconError):
app_icons.load_catalog(directory)
def test_more_icons_than_slots_are_refused(self):
icons = [Icon("i%02d" % n, "T", png(256, 256)) for n in range(len(app_icons.SLOTS) + 1)]
xml = Axml.parse(self.apk.read("AndroidManifest.xml"))
with tempfile.TemporaryDirectory() as folder:
directory = Path(folder)
(directory / "p.png").write_bytes(png(256, 256))
(directory / "catalog.tsv").write_text(
"".join("i%02d\tT\tp.png\n" % n for n in range(len(icons))), encoding="utf-8")
with self.assertRaises(IconError):
app_icons.load_catalog(directory)
if __name__ == "__main__":
unittest.main()