Update: 2026-07-04 23:44:47
This commit is contained in:
+13
@@ -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
|
||||
|
||||
+60
-28
@@ -74,11 +74,11 @@ 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
|
||||
}
|
||||
@@ -86,11 +86,12 @@ class FacebookNavigator(private val service: AccessibilityService) {
|
||||
}
|
||||
|
||||
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,15 +100,16 @@ 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() {
|
||||
@@ -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() ?: ""
|
||||
val candidates = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
|
||||
val displayMetrics = service.resources.displayMetrics
|
||||
val screenWidth = displayMetrics.widthPixels
|
||||
val screenHeight = displayMetrics.heightPixels
|
||||
|
||||
// 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)
|
||||
fun collectCandidates(n: android.view.accessibility.AccessibilityNodeInfo) {
|
||||
val className = n.className?.toString() ?: ""
|
||||
|
||||
if (!isHorizontal && node.isScrollable) {
|
||||
val rect = android.graphics.Rect()
|
||||
node.getBoundsInScreen(rect)
|
||||
val displayMetrics = service.resources.displayMetrics
|
||||
val isHorizontal = className.contains("ViewPager", ignoreCase = true) ||
|
||||
className.contains("Horizontal", ignoreCase = true)
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user