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 } }