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 allCollectedTexts = mutableSetOf<String>()
|
||||||
val totalIterations = 20 // 20 iterations * ~15 sec = ~5 minutes of scrolling
|
val totalIterations = 20 // 20 iterations * ~15 sec = ~5 minutes of scrolling
|
||||||
var iterations = 0
|
var iterations = 0
|
||||||
|
var consecutiveNoNewPosts = 0
|
||||||
|
|
||||||
while (iterations < totalIterations) {
|
while (iterations < totalIterations) {
|
||||||
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
|
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
|
||||||
|
val initialSize = allCollectedTexts.size
|
||||||
|
|
||||||
// 1. Expand texts
|
// 1. Expand texts
|
||||||
commentReader.expandPostText()
|
commentReader.expandPostText()
|
||||||
@@ -136,6 +138,17 @@ class FacebookBotService : AccessibilityService() {
|
|||||||
allCollectedTexts.addAll(posts)
|
allCollectedTexts.addAll(posts)
|
||||||
Log.d(TAG, "Collected ${posts.size} texts in this iteration. Total unique so far: ${allCollectedTexts.size}")
|
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...")
|
Log.d(TAG, "Scrolling down for more posts...")
|
||||||
navigator.scrollForward()
|
navigator.scrollForward()
|
||||||
delay(4000) // wait for new posts to load
|
delay(4000) // wait for new posts to load
|
||||||
|
|||||||
+68
-36
@@ -74,23 +74,24 @@ class FacebookNavigator(private val service: AccessibilityService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun scrollForward(): Boolean {
|
suspend fun scrollForward(): Boolean {
|
||||||
// Scroll 2 times using DOM to guarantee we pass large posts
|
|
||||||
var success = false
|
var success = false
|
||||||
val root = service.rootInActiveWindow ?: return false
|
|
||||||
|
// DOM scroll attempts — refetch root each iteration to avoid stale nodes
|
||||||
for (i in 0 until 2) {
|
for (i in 0 until 3) {
|
||||||
|
val root = service.rootInActiveWindow ?: continue
|
||||||
if (performDomScroll(root)) {
|
if (performDomScroll(root)) {
|
||||||
success = true
|
success = true
|
||||||
}
|
}
|
||||||
kotlinx.coroutines.delay(1000)
|
kotlinx.coroutines.delay(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
android.util.Log.d("FacebookNavigator", "DOM Scroll failed or found nothing, using Gesture Fallback!")
|
android.util.Log.d("FacebookNavigator", "DOM Scroll failed, using Gesture Fallback!")
|
||||||
// Do 2 gestures just to be sure we move past the post
|
kotlinx.coroutines.delay(800)
|
||||||
for (i in 0 until 2) {
|
// More attempts with moderate delay to let Facebook process each scroll
|
||||||
|
for (i in 0 until 4) {
|
||||||
success = performGestureScroll()
|
success = performGestureScroll()
|
||||||
kotlinx.coroutines.delay(1000)
|
kotlinx.coroutines.delay(1200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -99,17 +100,18 @@ class FacebookNavigator(private val service: AccessibilityService) {
|
|||||||
private suspend fun performGestureScroll(): Boolean = kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
|
private suspend fun performGestureScroll(): Boolean = kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
|
||||||
val displayMetrics = service.resources.displayMetrics
|
val displayMetrics = service.resources.displayMetrics
|
||||||
val middleX = displayMetrics.widthPixels / 2f
|
val middleX = displayMetrics.widthPixels / 2f
|
||||||
val startY = displayMetrics.heightPixels * 0.8f // Start near bottom
|
// Moderate swipe to avoid accidental pull-to-refresh; longer duration for reliability
|
||||||
val endY = displayMetrics.heightPixels * 0.2f // Swipe up to near top
|
val startY = displayMetrics.heightPixels * 0.75f
|
||||||
|
val endY = displayMetrics.heightPixels * 0.35f
|
||||||
|
|
||||||
val path = android.graphics.Path()
|
val path = android.graphics.Path()
|
||||||
path.moveTo(middleX, startY)
|
path.moveTo(middleX, startY)
|
||||||
path.lineTo(middleX, endY)
|
path.lineTo(middleX, endY)
|
||||||
|
|
||||||
val gesture = android.accessibilityservice.GestureDescription.Builder()
|
val gesture = android.accessibilityservice.GestureDescription.Builder()
|
||||||
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 500))
|
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 1200))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val dispatched = service.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
|
val dispatched = service.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
|
||||||
override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) {
|
override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) {
|
||||||
if (continuation.isActive) continuation.resume(true, null)
|
if (continuation.isActive) continuation.resume(true, null)
|
||||||
@@ -118,7 +120,7 @@ class FacebookNavigator(private val service: AccessibilityService) {
|
|||||||
if (continuation.isActive) continuation.resume(false, null)
|
if (continuation.isActive) continuation.resume(false, null)
|
||||||
}
|
}
|
||||||
}, null)
|
}, null)
|
||||||
|
|
||||||
if (!dispatched) {
|
if (!dispatched) {
|
||||||
if (continuation.isActive) continuation.resume(false, null)
|
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 {
|
private fun performDomScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
|
||||||
if (node == null) return false
|
if (node == null) return false
|
||||||
|
|
||||||
val className = node.className?.toString() ?: ""
|
val candidates = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
|
||||||
|
val displayMetrics = service.resources.displayMetrics
|
||||||
// CRITICAL FIX: ViewPager responds to SCROLL_FORWARD by swiping tabs horizontally!
|
val screenWidth = displayMetrics.widthPixels
|
||||||
// We MUST ignore ViewPagers to prevent the bot from changing tabs to the Menu.
|
val screenHeight = displayMetrics.heightPixels
|
||||||
val isHorizontal = className.contains("ViewPager", ignoreCase = true) ||
|
|
||||||
className.contains("Horizontal", ignoreCase = true)
|
|
||||||
|
|
||||||
if (!isHorizontal && node.isScrollable) {
|
fun collectCandidates(n: android.view.accessibility.AccessibilityNodeInfo) {
|
||||||
val rect = android.graphics.Rect()
|
val className = n.className?.toString() ?: ""
|
||||||
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) {
|
if (!isHorizontal) {
|
||||||
val scrolled = node.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
|
val rect = android.graphics.Rect()
|
||||||
if (scrolled) {
|
n.getBoundsInScreen(rect)
|
||||||
return true
|
|
||||||
|
// 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) {
|
for (i in 0 until n.childCount) {
|
||||||
if (performDomScroll(node.getChild(i))) {
|
n.getChild(i)?.let { collectCandidates(it) }
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user