Update: 2026-07-04 23:44:47

This commit is contained in:
Hamza-Ayed
2026-07-04 23:44:48 +03:00
parent 02bc56164c
commit a0e9bd3cbb
2 changed files with 81 additions and 36 deletions
@@ -102,9 +102,11 @@ class FacebookBotService : AccessibilityService() {
val allCollectedTexts = mutableSetOf<String>()
val totalIterations = 20 // 20 iterations * ~15 sec = ~5 minutes of scrolling
var iterations = 0
var consecutiveNoNewPosts = 0
while (iterations < totalIterations) {
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
val initialSize = allCollectedTexts.size
// 1. Expand texts
commentReader.expandPostText()
@@ -136,6 +138,17 @@ class FacebookBotService : AccessibilityService() {
allCollectedTexts.addAll(posts)
Log.d(TAG, "Collected ${posts.size} texts in this iteration. Total unique so far: ${allCollectedTexts.size}")
if (allCollectedTexts.size == initialSize) {
consecutiveNoNewPosts++
Log.d(TAG, "No new posts found in this iteration. (Streak: $consecutiveNoNewPosts)")
if (consecutiveNoNewPosts >= 3) {
Log.d(TAG, "No new posts for 3 consecutive iterations. Reached bottom or stuck. Breaking early.")
break
}
} else {
consecutiveNoNewPosts = 0
}
Log.d(TAG, "Scrolling down for more posts...")
navigator.scrollForward()
delay(4000) // wait for new posts to load
@@ -74,23 +74,24 @@ class FacebookNavigator(private val service: AccessibilityService) {
}
suspend fun scrollForward(): Boolean {
// Scroll 2 times using DOM to guarantee we pass large posts
var success = false
val root = service.rootInActiveWindow ?: return false
for (i in 0 until 2) {
// DOM scroll attempts — refetch root each iteration to avoid stale nodes
for (i in 0 until 3) {
val root = service.rootInActiveWindow ?: continue
if (performDomScroll(root)) {
success = true
}
kotlinx.coroutines.delay(1000)
}
if (!success) {
android.util.Log.d("FacebookNavigator", "DOM Scroll failed or found nothing, using Gesture Fallback!")
// Do 2 gestures just to be sure we move past the post
for (i in 0 until 2) {
android.util.Log.d("FacebookNavigator", "DOM Scroll failed, using Gesture Fallback!")
kotlinx.coroutines.delay(800)
// More attempts with moderate delay to let Facebook process each scroll
for (i in 0 until 4) {
success = performGestureScroll()
kotlinx.coroutines.delay(1000)
kotlinx.coroutines.delay(1200)
}
}
return success
@@ -99,17 +100,18 @@ class FacebookNavigator(private val service: AccessibilityService) {
private suspend fun performGestureScroll(): Boolean = kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
val displayMetrics = service.resources.displayMetrics
val middleX = displayMetrics.widthPixels / 2f
val startY = displayMetrics.heightPixels * 0.8f // Start near bottom
val endY = displayMetrics.heightPixels * 0.2f // Swipe up to near top
// Moderate swipe to avoid accidental pull-to-refresh; longer duration for reliability
val startY = displayMetrics.heightPixels * 0.75f
val endY = displayMetrics.heightPixels * 0.35f
val path = android.graphics.Path()
path.moveTo(middleX, startY)
path.lineTo(middleX, endY)
val gesture = android.accessibilityservice.GestureDescription.Builder()
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 500))
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 1200))
.build()
val dispatched = service.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) {
if (continuation.isActive) continuation.resume(true, null)
@@ -118,7 +120,7 @@ class FacebookNavigator(private val service: AccessibilityService) {
if (continuation.isActive) continuation.resume(false, null)
}
}, null)
if (!dispatched) {
if (continuation.isActive) continuation.resume(false, null)
}
@@ -132,32 +134,62 @@ class FacebookNavigator(private val service: AccessibilityService) {
private fun performDomScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
if (node == null) return false
val className = node.className?.toString() ?: ""
// CRITICAL FIX: ViewPager responds to SCROLL_FORWARD by swiping tabs horizontally!
// We MUST ignore ViewPagers to prevent the bot from changing tabs to the Menu.
val isHorizontal = className.contains("ViewPager", ignoreCase = true) ||
className.contains("Horizontal", ignoreCase = true)
val candidates = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
val displayMetrics = service.resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val screenHeight = displayMetrics.heightPixels
if (!isHorizontal && node.isScrollable) {
val rect = android.graphics.Rect()
node.getBoundsInScreen(rect)
val displayMetrics = service.resources.displayMetrics
// The main feed is a LARGE vertical container (height > width AND height > 40% of screen).
if (rect.height() > rect.width() && rect.height() > displayMetrics.heightPixels * 0.4f) {
val scrolled = node.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (scrolled) {
return true
fun collectCandidates(n: android.view.accessibility.AccessibilityNodeInfo) {
val className = n.className?.toString() ?: ""
val isHorizontal = className.contains("ViewPager", ignoreCase = true) ||
className.contains("Horizontal", ignoreCase = true)
if (!isHorizontal) {
val rect = android.graphics.Rect()
n.getBoundsInScreen(rect)
// Main feed: vertical, tall, spans full screen width.
// isScrollable is deliberately NOT required — Facebook often sets it to false.
if (rect.height() > rect.width() &&
rect.height() > screenHeight * 0.3f &&
rect.width() >= screenWidth * 0.85f) {
candidates.add(n)
}
}
}
for (i in 0 until node.childCount) {
if (performDomScroll(node.getChild(i))) {
return true
for (i in 0 until n.childCount) {
n.getChild(i)?.let { collectCandidates(it) }
}
}
collectCandidates(node)
// Prefer RecyclerView/ListView, then by largest area
candidates.sortWith(Comparator { a, b ->
fun isFeedCandidate(c: android.view.accessibility.AccessibilityNodeInfo): Boolean {
val cn = c.className?.toString() ?: ""
return cn.contains("RecyclerView", ignoreCase = true) ||
cn.contains("ListView", ignoreCase = true)
}
val aPref = isFeedCandidate(a)
val bPref = isFeedCandidate(b)
if (aPref != bPref) return@Comparator if (aPref) -1 else 1
val aRect = android.graphics.Rect().also { a.getBoundsInScreen(it) }
val bRect = android.graphics.Rect().also { b.getBoundsInScreen(it) }
(bRect.width() * bRect.height()).compareTo(aRect.width() * aRect.height())
})
for (candidate in candidates) {
try {
val scrolled = candidate.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (scrolled) return true
} catch (_: Exception) {
// performAction may throw on non-scrollable nodes
}
}
return false
}