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>
This commit is contained in:
172
tests/test_app_icons.py
Normal file
172
tests/test_app_icons.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user