Update: 2026-07-04 23:33:16
This commit is contained in:
@@ -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"
|
||||
|
||||
+41
-8
@@ -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
|
||||
|
||||
+105
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+99
-10
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user