From 8e6976fd7572a9c885581cc8db0be0636fff770e Mon Sep 17 00:00:00 2001 From: Doji Date: Sat, 17 Oct 2026 19:07:05 +0800 Subject: [PATCH] refactor: simplify timeline filtering - Drop reflective UrtTimelineModule reconstruction; a module containing a promoted entry is now dropped as a whole. - Remove the redundant isFirstPackage check; the package name check and onModuleLoaded detach already restrict filtering to the target process. - Drop the deprecated android:allowBackup attribute (the module stores no user data) and fix the stale hook comment. - Update tests to pin the new whole-module drop behavior. --- app/src/main/AndroidManifest.xml | 1 - app/src/main/kotlin/AdXModule.kt | 4 +- app/src/main/kotlin/TimelineFilter.kt | 35 +++++---------- app/src/test/kotlin/TimelineFilterTest.kt | 52 +++++++---------------- 4 files changed, 27 insertions(+), 65 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cda27a2..db75135 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/kotlin/AdXModule.kt b/app/src/main/kotlin/AdXModule.kt index 2bcaffa..59b4b1c 100644 --- a/app/src/main/kotlin/AdXModule.kt +++ b/app/src/main/kotlin/AdXModule.kt @@ -16,7 +16,7 @@ class AdXModule : XposedModule() { } override fun onPackageReady(param: PackageReadyParam) { - if (param.packageName != TARGET_PACKAGE || !param.isFirstPackage) return + if (param.packageName != TARGET_PACKAGE) return System.loadLibrary("dexkit") DexKitBridge.create(param.applicationInfo.sourceDir).use { bridge -> @@ -34,7 +34,7 @@ class AdXModule : XposedModule() { returnType(metadata.name) } }.single() - // Both update paths call this after merging cached items. + // URT applies this to the merged timeline list before rendering. val listMethod = bridge .findMethod { diff --git a/app/src/main/kotlin/TimelineFilter.kt b/app/src/main/kotlin/TimelineFilter.kt index d2d5fd0..2f4ae6b 100644 --- a/app/src/main/kotlin/TimelineFilter.kt +++ b/app/src/main/kotlin/TimelineFilter.kt @@ -16,37 +16,22 @@ internal class TimelineFilter( } private val promotedUserMetadataField = promotedUserClass.instanceFields().single { it.type == promotedMetadataMethod.returnType } - private val moduleFields = moduleClass.instanceFields() - private val moduleContentField = moduleFields.single { it.type == List::class.java } - private val moduleConstructor = - moduleClass.declaredConstructors - .single { constructor -> - constructor.parameterTypes.all { type -> moduleFields.any { it.type == type } } - }.apply { isAccessible = true } - private val moduleConstructorFields = - moduleConstructor.parameterTypes.map { type -> moduleFields.single { it.type == type } } + private val moduleContentField = + moduleClass.instanceFields().single { it.type == List::class.java } init { promotedMetadataMethod.isAccessible = true } - fun filter(source: List<*>): List<*> = - source.mapNotNull { item -> - if (isPromoted(item)) return@mapNotNull null - if (!moduleClass.isInstance(item)) return@mapNotNull item + fun filter(source: List<*>): List<*> = source.filterNot(::isPromotedItem) - val content = moduleContentField.get(item) as List<*> - val filtered = content.filterNot { isPromoted(moduleItemField.get(it)) } - if (filtered.size == content.size) return@mapNotNull item - if (filtered.isEmpty()) return@mapNotNull null - - moduleConstructor.newInstance( - *moduleConstructorFields - .map { field -> - if (field == moduleContentField) filtered else field.get(item) - }.toTypedArray(), - ) - } + // Drop the whole module instead of rebuilding it when one of its entries is promoted. + private fun isPromotedItem(item: Any?): Boolean { + if (isPromoted(item)) return true + if (!moduleClass.isInstance(item)) return false + val content = moduleContentField.get(item) as List<*> + return content.any { isPromoted(moduleItemField.get(it)) } + } private fun isPromoted(item: Any?): Boolean = if (promotedUserClass.isInstance(item)) { diff --git a/app/src/test/kotlin/TimelineFilterTest.kt b/app/src/test/kotlin/TimelineFilterTest.kt index 1d12779..44966a6 100644 --- a/app/src/test/kotlin/TimelineFilterTest.kt +++ b/app/src/test/kotlin/TimelineFilterTest.kt @@ -1,7 +1,6 @@ package dev.doji.adx import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotSame import org.junit.Assert.assertSame import org.junit.Assert.assertThrows import org.junit.Test @@ -33,35 +32,12 @@ class TimelineFilterTest { } @Test - fun `copies mixed modules and preserves other fields and retained wrappers`() { - val post = ModuleItem(Post(null), true) - val user = ModuleItem(User(null), false) - val promotedPost = ModuleItem(Post(Metadata()), false) - val promotedUser = ModuleItem(User(Metadata()), true) - val content = listOf(promotedPost, post, promotedUser, user) - val module = Module(content, 42, "recommendations") - - val result = filter.filter(listOf(module)).single() as Module - - assertNotSame(module, result) - assertEquals(listOf(post, user), result.content) - assertEquals(module.id, result.id) - assertEquals(module.label, result.label) - assertEquals(2, result.cachedContentSize) - assertSame(content, module.content) - assertEquals(listOf(promotedPost, post, promotedUser, user), module.content) - assertEquals(4, module.cachedContentSize) - } - - @Test - fun `removes all promoted modules and reuses unchanged modules`() { + fun `drops modules containing promoted items and reuses unchanged modules`() { val promoted = Module( - listOf(ModuleItem(Post(Metadata()), false), ModuleItem(User(Metadata()), false)), - 1, - "promoted", + listOf(ModuleItem(Post(Metadata())), ModuleItem(User(Metadata()))), ) - val ordinary = Module(listOf(ModuleItem(Post(null), true)), 2, "ordinary") + val ordinary = Module(listOf(ModuleItem(Post(null)))) val result = filter.filter(listOf(promoted, ordinary)) @@ -69,14 +45,22 @@ class TimelineFilterTest { assertEquals(2, promoted.content.size) } + @Test + fun `drops a mixed module instead of only its promoted entries`() { + val organic = ModuleItem(Post(null)) + val promoted = ModuleItem(Post(Metadata())) + val module = Module(listOf(organic, promoted)) + + assertEquals(emptyList(), filter.filter(listOf(module))) + assertEquals(listOf(organic, promoted), module.content) + } + @Test fun `propagates accessor failures without mutating source modules`() { val failure = IllegalStateException("metadata unavailable") val module = Module( - listOf(ModuleItem(Post(Metadata()), false), ModuleItem(FailedItem(failure), false)), - 3, - "failed", + listOf(ModuleItem(Post(null)), ModuleItem(FailedItem(failure))), ) val source = listOf(module) val content = module.content @@ -87,7 +71,6 @@ class TimelineFilterTest { assertSame(module, source.single()) assertSame(content, module.content) assertEquals(2, module.content.size) - assertEquals(2, module.cachedContentSize) } private interface Item @@ -108,16 +91,11 @@ class TimelineFilterTest { private class ModuleItem( val item: Item, - val flag: Boolean, ) private class Module( val content: List, - val id: Long, - val label: String, - ) : Item { - val cachedContentSize = content.size - } + ) : Item companion object { @JvmStatic