Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="false"
android:label="AdX"
tools:ignore="MissingApplicationIcon" />
</manifest>
4 changes: 2 additions & 2 deletions app/src/main/kotlin/AdXModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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 {
Expand Down
35 changes: 10 additions & 25 deletions app/src/main/kotlin/TimelineFilter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
52 changes: 15 additions & 37 deletions app/src/test/kotlin/TimelineFilterTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -33,50 +32,35 @@ 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))

assertSame(ordinary, result.single())
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<Any>(), 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
Expand All @@ -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
Expand All @@ -108,16 +91,11 @@ class TimelineFilterTest {

private class ModuleItem(
val item: Item,
val flag: Boolean,
)

private class Module(
val content: List<ModuleItem>,
val id: Long,
val label: String,
) : Item {
val cachedContentSize = content.size
}
) : Item

companion object {
@JvmStatic
Expand Down