diff --git a/socialBot/app/src/main/AndroidManifest.xml b/socialBot/app/src/main/AndroidManifest.xml
index 10711e71..75e6d501 100644
--- a/socialBot/app/src/main/AndroidManifest.xml
+++ b/socialBot/app/src/main/AndroidManifest.xml
@@ -4,6 +4,16 @@
+
+
+
+
+
+
+
+
+
+
()
+ val totalIterations = 20 // 20 iterations * ~15 sec = ~5 minutes of scrolling
var iterations = 0
- while (iterations < 5) {
- Log.d(TAG, "Autonomous Iteration: ${iterations + 1}")
- val posts = commentReader.extractPostsAndComments()
+
+ while (iterations < totalIterations) {
+ Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
- if (posts.isNotEmpty()) {
- Log.d(TAG, "Found ${posts.size} posts. Sending to Gemini for reporting...")
- SocialBotClient.evaluatePosts(posts)
+ // 1. Expand texts
+ commentReader.expandPostText()
+ delay(1000)
+
+ // 2. Read feed posts
+ val posts = commentReader.extractPostsAndComments().toMutableList()
+
+ // 3. Open comments if available
+ if (commentReader.openComments()) {
+ Log.d(TAG, "Opened comments section. Reading...")
+ delay(4000) // wait for bottom sheet to load
+
+ // 4. Expand long comments
+ commentReader.expandPostText()
+ delay(1000)
+
+ // 5. Read comments
+ val comments = commentReader.extractPostsAndComments()
+ posts.addAll(comments)
+
+ // 6. Go back to feed
+ Log.d(TAG, "Going back to main feed...")
+ navigator.goBack()
+ delay(3000)
}
+ // Add to our global collection (Set avoids duplicates)
+ allCollectedTexts.addAll(posts)
+ Log.d(TAG, "Collected ${posts.size} texts in this iteration. Total unique so far: ${allCollectedTexts.size}")
+
Log.d(TAG, "Scrolling down for more posts...")
navigator.scrollForward()
delay(4000) // wait for new posts to load
iterations++
}
- SocialBotClient.completeTask(task.id, "Autonomous session completed")
+ // Send everything to Gemini AT ONCE to save API costs and generate a comprehensive report
+ if (allCollectedTexts.isNotEmpty()) {
+ Log.d(TAG, "Finished scrolling. Sending ${allCollectedTexts.size} total texts to Gemini for one comprehensive report...")
+ SocialBotClient.evaluatePosts(allCollectedTexts.toList())
+ }
+
+ SocialBotClient.completeTask(task.id, "Autonomous session completed. Scraped ${allCollectedTexts.size} items.")
}
"read_posts" -> {
// Logic to read posts and send to backend
diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt
index 649279b6..4083c409 100644
--- a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt
+++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt
@@ -14,12 +14,21 @@ class FacebookCommentReader(private val service: AccessibilityService) {
findTextNodes(root, uniqueTexts)
extractedTexts.addAll(uniqueTexts)
+
+ // Print to Logcat for debugging purposes
+ for (text in extractedTexts) {
+ android.util.Log.d("ScrapedText", "Extracted: $text")
+ }
+
return extractedTexts
}
private fun findTextNodes(node: AccessibilityNodeInfo?, collectedTexts: MutableSet) {
if (node == null) return
+ // Skip nodes that are off-screen or hidden to avoid reading background side-menus
+ if (!node.isVisibleToUser) return
+
val text = node.text?.toString() ?: node.contentDescription?.toString()
if (!text.isNullOrBlank()) {
@@ -88,4 +97,100 @@ class FacebookCommentReader(private val service: AccessibilityService) {
return true
}
+
+ fun expandPostText() {
+ val root = service.rootInActiveWindow ?: return
+ val expandNodes = mutableListOf()
+ findNodesByText(root, "عرض المزيد", expandNodes)
+ findNodesByText(root, "See more", expandNodes)
+ findNodesByText(root, "قراءة المزيد", expandNodes)
+
+ for (node in expandNodes) {
+ node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+ }
+ }
+
+ fun openComments(): Boolean {
+ val root = service.rootInActiveWindow ?: return false
+ val commentNodes = mutableListOf()
+
+ // Find nodes containing "تعليق" or "comment" in either text or description
+ findNodesByPartialText(root, "تعليق", commentNodes)
+ findNodesByPartialText(root, "Comment", commentNodes)
+
+ // Prioritize nodes that look like "5 تعليقات" or exact "تعليق"
+ commentNodes.sortBy {
+ val text = (it.text?.toString() ?: "") + (it.contentDescription?.toString() ?: "")
+ if (text.matches(Regex(".*\\d+.*(تعليق|comment).*"))) 0 else 1
+ }
+
+ val displayMetrics = service.resources.displayMetrics
+ val minAcceptableY = displayMetrics.heightPixels * 0.35f // Ignore top 35% of the screen (old posts)
+
+ for (node in commentNodes) {
+ val rect = android.graphics.Rect()
+ node.getBoundsInScreen(rect)
+
+ // If the comment button is too high on the screen, it belongs to the previous post!
+ if (rect.bottom < minAcceptableY) {
+ continue
+ }
+
+ val textStr = node.text?.toString() ?: ""
+ val descStr = node.contentDescription?.toString() ?: ""
+ val combined = "$textStr $descStr".toLowerCase()
+
+ // Avoid clicking the "write a comment" input box, the combined "Like, Comment, Share" container, or the Menu tab
+ if (combined.contains("اكتب") || combined.contains("write") ||
+ combined.contains("أعجبني") || combined.contains("like") ||
+ combined.contains("مشاركة") || combined.contains("share") ||
+ combined.contains("قائمة") || combined.contains("menu")) {
+ continue
+ }
+
+ // Try clicking the node
+ if (node.isClickable && node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) return true
+
+ // Try clicking its parent (often icons are inside clickable containers)
+ // Limit depth to 1 to avoid clicking massive screen containers like the main navigation bar
+ var parent = node.parent
+ var depth = 0
+ while (parent != null && depth < 1) {
+ if (parent.isClickable && parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
+ return true
+ }
+ parent = parent.parent
+ depth++
+ }
+ }
+ return false
+ }
+
+ private fun findNodesByText(node: AccessibilityNodeInfo?, target: String, list: MutableList) {
+ if (node == null) return
+ if (!node.isVisibleToUser) return
+
+ val textStr = node.text?.toString() ?: ""
+ val descStr = node.contentDescription?.toString() ?: ""
+ if (textStr.equals(target, ignoreCase = true) || descStr.equals(target, ignoreCase = true)) {
+ list.add(node)
+ }
+ for (i in 0 until node.childCount) {
+ findNodesByText(node.getChild(i), target, list)
+ }
+ }
+
+ private fun findNodesByPartialText(node: AccessibilityNodeInfo?, target: String, list: MutableList) {
+ if (node == null) return
+ if (!node.isVisibleToUser) return
+
+ val textStr = node.text?.toString() ?: ""
+ val descStr = node.contentDescription?.toString() ?: ""
+ if (textStr.contains(target, ignoreCase = true) || descStr.contains(target, ignoreCase = true)) {
+ list.add(node)
+ }
+ for (i in 0 until node.childCount) {
+ findNodesByPartialText(node.getChild(i), target, list)
+ }
+ }
}
diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt
index 3b683abb..5f574ad7 100644
--- a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt
+++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt
@@ -10,6 +10,7 @@ class FacebookNavigator(private val service: AccessibilityService) {
suspend fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.setPackage("com.facebook.katana") // Open specifically in Facebook app
try {
@@ -25,13 +26,34 @@ class FacebookNavigator(private val service: AccessibilityService) {
}
suspend fun openApp() {
- val launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.katana")
+ // Kill the app first so it restarts with a fresh feed
+ val am = service.getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager
+ am.killBackgroundProcesses("com.facebook.katana")
+ am.killBackgroundProcesses("com.facebook.lite")
+ kotlinx.coroutines.delay(1000)
+
+ var launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.katana")
+ if (launchIntent == null) {
+ launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.lite")
+ }
+
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
service.startActivity(launchIntent)
- delay(5000) // Wait 5 seconds for Facebook to load completely
+ delay(8000) // Wait 8 seconds for Facebook to load completely
- // Try to navigate to Groups Tab automatically
+ // Explicitly click the Home tab to ensure we are on the main feed
+ val root = service.rootInActiveWindow
+ if (root != null) {
+ val homeTab = findNodeByContentDescription(root, "الرئيسية") ?: findNodeByContentDescription(root, "Home")
+ if (homeTab != null) {
+ homeTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ delay(3000)
+ }
+ }
+
+ // Try to navigate to Groups Tab automatically (if available)
openGroupsTab()
} else {
android.util.Log.e("FacebookNavigator", "Facebook app is not installed.")
@@ -47,25 +69,92 @@ class FacebookNavigator(private val service: AccessibilityService) {
if (groupsTab != null) {
groupsTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
- delay(3000) // wait for Groups tab to load
+ delay(4000) // wait for Groups tab to load
}
}
suspend fun scrollForward(): Boolean {
+ // Scroll 2 times using DOM to guarantee we pass large posts
+ var success = false
val root = service.rootInActiveWindow ?: return false
- return performScroll(root)
+
+ for (i in 0 until 2) {
+ 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) {
+ success = performGestureScroll()
+ kotlinx.coroutines.delay(1000)
+ }
+ }
+ return success
}
- private fun performScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
+ 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
+
+ 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))
+ .build()
+
+ val dispatched = service.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
+ override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) {
+ if (continuation.isActive) continuation.resume(true, null)
+ }
+ override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) {
+ if (continuation.isActive) continuation.resume(false, null)
+ }
+ }, null)
+
+ if (!dispatched) {
+ if (continuation.isActive) continuation.resume(false, null)
+ }
+ }
+
+ suspend fun goBack() {
+ service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
+ kotlinx.coroutines.delay(2000) // Wait for bottom sheet to close
+ }
+
+ private fun performDomScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
if (node == null) return false
- if (node.isScrollable && node.className?.toString()?.contains("RecyclerView") == true) {
- val scrolled = node.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
- if (scrolled) return true
+ 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)
+
+ 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
+ }
+ }
}
for (i in 0 until node.childCount) {
- if (performScroll(node.getChild(i))) {
+ if (performDomScroll(node.getChild(i))) {
return true
}
}