From 39ec3933b139e1fa4180e14aee1a468d70d370d4 Mon Sep 17 00:00:00 2001 From: havenmab Date: Wed, 30 Sep 2026 22:14:20 +0800 Subject: [PATCH 1/2] fix(lyrics): pin the 6.5.2 current lyrics item owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 6.5.2 profile pinned com.apple.android.music.player.fragment.m, which declares no matching field on the real APK, so the symbol fell through to the structural scan and failed closed with two candidates. Both current_song_identity and custom_lyrics inherit that failure, which is why the settings page's "获取 ID" button always claimed no song was playing and custom lyrics never replaced anything on 6.5.2. The correct owner is fragment.l: it is where the field named `c` that isLyricsCurrentItemField already requires lives, and 6.5.1 pins the same owner. --- .../amenhancer/module/hook/TargetSymbols.kt | 8 ++- ...AppleMusic652LyricsCurrentItemFieldTest.kt | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt diff --git a/app/src/main/java/dev/amenhancer/module/hook/TargetSymbols.kt b/app/src/main/java/dev/amenhancer/module/hook/TargetSymbols.kt index 9dc654ff..3ad7ca2b 100644 --- a/app/src/main/java/dev/amenhancer/module/hook/TargetSymbols.kt +++ b/app/src/main/java/dev/amenhancer/module/hook/TargetSymbols.kt @@ -431,8 +431,14 @@ private object AppleMusicProfiles { "com.apple.android.music.ttml.javanative.model.SongInfo\$SongInfoNative", TargetSymbolId.TTML_PARSER_NATIVE to "com.apple.android.music.ttml.javanative.TTMLParser\$TTMLParserNative", + // 6.5.2 (1586) device log: resolving this symbol reported two structural candidates, + // `player.fragment.e#U` and `player.fragment.l#c`, which means the previous pin + // (`player.fragment.m`) declares no matching field on the real APK and never produced + // a profile hit. `player.fragment.l#c` is exactly the field the + // `isLyricsCurrentItemField` contract requires (non-static, name "c", type + // BaseContentItem), and 6.5.1 independently pins the same owner. TargetSymbolId.LYRICS_CURRENT_ITEM_FIELD to - "com.apple.android.music.player.fragment.m", + "com.apple.android.music.player.fragment.l", TargetSymbolId.PLAYER_METADATA_HUB to "com.apple.android.music.player.f", TargetSymbolId.METADATA_TO_ITEM_CONVERTER to "com.apple.android.music.player.O", TargetSymbolId.LYRICS_AVAILABILITY_OWNER to "com.apple.android.music.player.e1", diff --git a/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt b/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt new file mode 100644 index 00000000..0f839903 --- /dev/null +++ b/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt @@ -0,0 +1,60 @@ +package dev.amenhancer.module.hook + +import com.apple.android.music.model.BaseContentItem +import dev.amenhancer.module.ModuleConstants +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Apple Music 6.5.2 (1586) adaptation: the profile pinned `player.fragment.m`, which declares no + * matching field on the real APK, so the symbol fell through to the structural scan and failed + * closed with two candidates (`player.fragment.e#U` and `player.fragment.l#c`). Pin the verified + * owner so both `current_song_identity` and `custom_lyrics` resolve at the profile layer. + */ +class AppleMusic652LyricsCurrentItemFieldTest { + private val build652 = TargetBuild(ModuleConstants.TARGET_PACKAGE, "6.5.2", 1586L) + + @Test + fun `6_5_2 profile pins fragment l instead of the stale fragment m`() { + // Only `fragment.l` is loadable, so a VERSION_PROFILE hit proves the pin names it; under + // the stale `fragment.m` pin the profile layer would yield nothing and fall through. + val source = Lyrics652FakeClassSource( + mapOf("com.apple.android.music.player.fragment.l" to Lyrics652Fixture::class.java), + ) + val resolution = IndexedTargetSymbolResolver(build652, source) + .resolve(AppleMusicSymbols.LyricsCurrentItemField) + + assertTrue(resolution is TargetResolution.Found) + assertEquals(SymbolMatch.VERSION_PROFILE, (resolution as TargetResolution.Found).match) + assertTrue(source.loadedNames.contains("com.apple.android.music.player.fragment.l")) + assertFalse(source.loadedNames.any { it == "com.apple.android.music.player.fragment.m" }) + // A profile hit never touches the structural scan. + assertEquals(0, source.classNameReads) + } +} + +/** 6.5.2 shape: a single non-static [BaseContentItem] field named `c`. */ +@Suppress("unused", "PropertyName") +private class Lyrics652Fixture { + val c: BaseContentItem = BaseContentItem() +} + +private class Lyrics652FakeClassSource( + private val classes: Map>, +) : TargetClassSource { + val loadedNames = mutableListOf() + var classNameReads = 0 + private set + + override fun classNames(): List { + classNameReads++ + return emptyList() + } + + override fun loadClass(name: String): Class<*>? { + loadedNames += name + return classes[name] + } +} From e6e8d71b1199dba891cf59dfa81767d1ec8e2ab8 Mon Sep 17 00:00:00 2001 From: Havenmab <72502111+Havenmab@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:52:50 +0800 Subject: [PATCH 2/2] Delete app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt --- ...AppleMusic652LyricsCurrentItemFieldTest.kt | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt diff --git a/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt b/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt deleted file mode 100644 index 0f839903..00000000 --- a/app/src/test/java/dev/amenhancer/module/hook/AppleMusic652LyricsCurrentItemFieldTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package dev.amenhancer.module.hook - -import com.apple.android.music.model.BaseContentItem -import dev.amenhancer.module.ModuleConstants -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -/** - * Apple Music 6.5.2 (1586) adaptation: the profile pinned `player.fragment.m`, which declares no - * matching field on the real APK, so the symbol fell through to the structural scan and failed - * closed with two candidates (`player.fragment.e#U` and `player.fragment.l#c`). Pin the verified - * owner so both `current_song_identity` and `custom_lyrics` resolve at the profile layer. - */ -class AppleMusic652LyricsCurrentItemFieldTest { - private val build652 = TargetBuild(ModuleConstants.TARGET_PACKAGE, "6.5.2", 1586L) - - @Test - fun `6_5_2 profile pins fragment l instead of the stale fragment m`() { - // Only `fragment.l` is loadable, so a VERSION_PROFILE hit proves the pin names it; under - // the stale `fragment.m` pin the profile layer would yield nothing and fall through. - val source = Lyrics652FakeClassSource( - mapOf("com.apple.android.music.player.fragment.l" to Lyrics652Fixture::class.java), - ) - val resolution = IndexedTargetSymbolResolver(build652, source) - .resolve(AppleMusicSymbols.LyricsCurrentItemField) - - assertTrue(resolution is TargetResolution.Found) - assertEquals(SymbolMatch.VERSION_PROFILE, (resolution as TargetResolution.Found).match) - assertTrue(source.loadedNames.contains("com.apple.android.music.player.fragment.l")) - assertFalse(source.loadedNames.any { it == "com.apple.android.music.player.fragment.m" }) - // A profile hit never touches the structural scan. - assertEquals(0, source.classNameReads) - } -} - -/** 6.5.2 shape: a single non-static [BaseContentItem] field named `c`. */ -@Suppress("unused", "PropertyName") -private class Lyrics652Fixture { - val c: BaseContentItem = BaseContentItem() -} - -private class Lyrics652FakeClassSource( - private val classes: Map>, -) : TargetClassSource { - val loadedNames = mutableListOf() - var classNameReads = 0 - private set - - override fun classNames(): List { - classNameReads++ - return emptyList() - } - - override fun loadClass(name: String): Class<*>? { - loadedNames += name - return classes[name] - } -}