Update: 2026-07-04 23:33:16

This commit is contained in:
Hamza-Ayed
2026-07-04 23:33:16 +03:00
parent 5f2aa84101
commit 02bc56164c
4 changed files with 255 additions and 18 deletions
@@ -4,6 +4,16 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<!-- Required for Android 11+ to check and launch external apps like Facebook -->
<queries>
<package android:name="com.facebook.katana" />
<package android:name="com.facebook.lite" />
<package android:name="com.instagram.android" />
<package android:name="com.twitter.android" />
<package android:name="com.zhiliaoapp.musically" />
</queries>
<application
android:allowBackup="true"
@@ -66,7 +66,7 @@ class FacebookBotService : AccessibilityService() {
executeTask(currentTask!!)
} else {
Log.d(TAG, "No tasks available. Sleeping...")
delay(60000) // Wait 1 minute before checking again
delay(3600000) // Wait 1 hour before checking again
}
} catch (e: Exception) {
Log.e(TAG, "Error in task loop", e)
@@ -99,23 +99,56 @@ class FacebookBotService : AccessibilityService() {
navigator.openApp()
delay(3000)
val allCollectedTexts = mutableSetOf<String>()
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
@@ -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<String>) {
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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
// 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<AccessibilityNodeInfo>) {
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<AccessibilityNodeInfo>) {
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)
}
}
}
@@ -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
}
}