Update: 2026-07-04 21:38:44
This commit is contained in:
@@ -133,7 +133,7 @@ class SiroGeminiService {
|
|||||||
*/
|
*/
|
||||||
public function analyzeCompetitorFormulas(
|
public function analyzeCompetitorFormulas(
|
||||||
array $formulas,
|
array $formulas,
|
||||||
string $model = 'gemini-1.5-flash'
|
string $model = 'gemini-flash-lite-latest'
|
||||||
): ?array {
|
): ?array {
|
||||||
if (!$this->apiKey) return null;
|
if (!$this->apiKey) return null;
|
||||||
|
|
||||||
@@ -249,5 +249,58 @@ class SiroGeminiService {
|
|||||||
error_log("[SiroGeminiService] Exception: " . $e->getMessage());
|
error_log("[SiroGeminiService] Exception: " . $e->getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
public function evaluatePostsForReporting(array $posts, string $model = 'gemini-flash-lite-latest'): ?string {
|
||||||
|
if (!$this->apiKey || empty($posts)) return null;
|
||||||
|
|
||||||
|
$postsJson = json_encode($posts, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$prompt = "
|
||||||
|
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
|
||||||
|
إليك مجموعة من المنشورات والتعليقات التي جمعها الروبوت الخاص بنا من مجموعات فيسبوك اليوم:
|
||||||
|
$postsJson
|
||||||
|
|
||||||
|
المطلوب منك:
|
||||||
|
1. قراءة جميع هذه المنشورات واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي، أسعار المحروقات، باقات الإنترنت، مشاكل السيارات).
|
||||||
|
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report).
|
||||||
|
3. تجاهل المنشورات العشوائية أو الشخصية التي لا تفيد السوق.
|
||||||
|
|
||||||
|
قم بصياغة تقريرك بتنسيق HTML مرتب وجاهز للعرض (بدون علامات ```html)، واستخدم <h3> للعناوين و <ul> للقوائم.
|
||||||
|
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <p>لا توجد بيانات مفيدة في هذه الدفعة.</p>
|
||||||
|
";
|
||||||
|
|
||||||
|
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
|
||||||
|
|
||||||
|
$postData = [
|
||||||
|
'contents' => [
|
||||||
|
[
|
||||||
|
'parts' => [
|
||||||
|
['text' => $prompt]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'generationConfig' => [
|
||||||
|
'temperature' => 0.5,
|
||||||
|
'maxOutputTokens' => 1500
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode === 200) {
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||||
|
return trim(preg_replace('/```html|```/', '', $text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
$con = Database::get('main');
|
||||||
|
|
||||||
|
// Insert the autonomous_scroll_and_reply task
|
||||||
|
$stmt = $con->prepare("INSERT INTO marketing_tasks (platform, type, status) VALUES ('facebook', 'autonomous_scroll_and_reply', 'pending')");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo "Task inserted successfully at " . date('Y-m-d H:i:s') . "\n";
|
||||||
@@ -6,29 +6,26 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../core/bootstrap.php';
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
require_once __DIR__ . '/../functions.php';
|
require_once __DIR__ . '/../functions.php';
|
||||||
|
require_once __DIR__ . '/../core/Services/SiroGeminiService.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Simple bot authentication
|
// Simple bot authentication
|
||||||
$headers = getallheaders();
|
$headers = getallheaders();
|
||||||
$botToken = $headers['X-Bot-Token'] ?? '';
|
$botToken = $headers['X-Bot-Token'] ?? '';
|
||||||
if ($botToken !== 'YOUR_SECRET_BOT_TOKEN') {
|
// if ($botToken !== 'YOUR_SECRET_BOT_TOKEN') {
|
||||||
// http_response_code(401);
|
// http_response_code(401);
|
||||||
// exit(json_encode(['status' => 'error', 'message' => 'Unauthorized']));
|
// exit(json_encode(['status' => 'error', 'message' => 'Unauthorized']));
|
||||||
}
|
// }
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
$action = $_GET['action'] ?? '';
|
||||||
$platform = $_GET['platform'] ?? 'tiktok'; // Default to tiktok for new platform testing
|
$platform = $_GET['platform'] ?? 'facebook';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In a real scenario, you'd use a dedicated database.
|
|
||||||
// Here we use the main DB but with marketing_ prefixed tables.
|
|
||||||
$con = Database::get('main');
|
$con = Database::get('main');
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'get_task':
|
case 'get_task':
|
||||||
// 1. First, check if the bot needs a task
|
|
||||||
// We look for pending tasks for this platform
|
|
||||||
$stmt = $con->prepare("
|
$stmt = $con->prepare("
|
||||||
SELECT id, type, target_url, content_text, media_url
|
SELECT id, type, target_url, content_text, media_url
|
||||||
FROM marketing_tasks
|
FROM marketing_tasks
|
||||||
@@ -39,10 +36,6 @@ try {
|
|||||||
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($task) {
|
if ($task) {
|
||||||
// If the task requires a media file (like upload_video)
|
|
||||||
// The Android app will need to download it first using the media_url
|
|
||||||
|
|
||||||
// Mark as in progress
|
|
||||||
$update = $con->prepare("UPDATE marketing_tasks SET status = 'in_progress' WHERE id = ?");
|
$update = $con->prepare("UPDATE marketing_tasks SET status = 'in_progress' WHERE id = ?");
|
||||||
$update->execute([$task['id']]);
|
$update->execute([$task['id']]);
|
||||||
|
|
||||||
@@ -78,6 +71,34 @@ try {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'evaluate_posts':
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$posts = $input['posts'] ?? [];
|
||||||
|
|
||||||
|
if (empty($posts)) {
|
||||||
|
echo json_encode(['status' => 'success', 'data' => null]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gemini = new SiroGeminiService();
|
||||||
|
$reportHtml = $gemini->evaluatePostsForReporting($posts);
|
||||||
|
|
||||||
|
if ($reportHtml) {
|
||||||
|
// Save to database
|
||||||
|
$con->query("CREATE TABLE IF NOT EXISTS `marketing_reports` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`platform` VARCHAR(50) NOT NULL,
|
||||||
|
`report_html` TEXT NOT NULL,
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)");
|
||||||
|
|
||||||
|
$stmt = $con->prepare("INSERT INTO marketing_reports (platform, report_html) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$platform, $reportHtml]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Posts evaluated and report saved']);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,43 @@ object SocialBotClient {
|
|||||||
postData("action=complete_task", "task_id=$taskId&result=$result")
|
postData("action=complete_task", "task_id=$taskId&result=$result")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun evaluatePosts(posts: List<String>): JSONObject? {
|
||||||
|
try {
|
||||||
|
val urlString = "$BASE_URL?action=evaluate_posts"
|
||||||
|
val url = URL(urlString)
|
||||||
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "POST"
|
||||||
|
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
|
connection.doOutput = true
|
||||||
|
|
||||||
|
val jsonBody = JSONObject()
|
||||||
|
val jsonArray = org.json.JSONArray(posts)
|
||||||
|
jsonBody.put("posts", jsonArray)
|
||||||
|
|
||||||
|
val writer = OutputStreamWriter(connection.outputStream)
|
||||||
|
writer.write(jsonBody.toString())
|
||||||
|
writer.flush()
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) {
|
||||||
|
val scanner = Scanner(connection.inputStream)
|
||||||
|
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
|
||||||
|
scanner.close()
|
||||||
|
|
||||||
|
val json = JSONObject(response)
|
||||||
|
if (json.getString("status") == "success" && !json.isNull("data")) {
|
||||||
|
return json.getJSONObject("data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SocialBotClient", "Exception in evaluatePosts: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun failTask(taskId: Int, errorMessage: String) {
|
fun failTask(taskId: Int, errorMessage: String) {
|
||||||
postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage")
|
postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage")
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -94,6 +94,29 @@ class FacebookBotService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"autonomous_scroll_and_reply" -> {
|
||||||
|
Log.d(TAG, "Starting autonomous mode. Opening Facebook app...")
|
||||||
|
navigator.openApp()
|
||||||
|
delay(3000)
|
||||||
|
|
||||||
|
var iterations = 0
|
||||||
|
while (iterations < 5) {
|
||||||
|
Log.d(TAG, "Autonomous Iteration: ${iterations + 1}")
|
||||||
|
val posts = commentReader.extractPostsAndComments()
|
||||||
|
|
||||||
|
if (posts.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "Found ${posts.size} posts. Sending to Gemini for reporting...")
|
||||||
|
SocialBotClient.evaluatePosts(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
"read_posts" -> {
|
"read_posts" -> {
|
||||||
// Logic to read posts and send to backend
|
// Logic to read posts and send to backend
|
||||||
Log.d(TAG, "Starting to scan screen for posts/comments...")
|
Log.d(TAG, "Starting to scan screen for posts/comments...")
|
||||||
|
|||||||
+74
-10
@@ -11,37 +11,72 @@ import kotlinx.coroutines.delay
|
|||||||
class FacebookCommentPoster(private val service: AccessibilityService) {
|
class FacebookCommentPoster(private val service: AccessibilityService) {
|
||||||
|
|
||||||
suspend fun postComment(commentText: String): Boolean {
|
suspend fun postComment(commentText: String): Boolean {
|
||||||
|
return performCommentPosting(commentText)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postCommentOnSpecificPost(originalText: String, commentText: String): Boolean {
|
||||||
|
val root = service.rootInActiveWindow ?: return false
|
||||||
|
|
||||||
|
// 1. Find the specific post node
|
||||||
|
val postNode = findNodeByExactText(root, originalText)
|
||||||
|
if (postNode == null) {
|
||||||
|
android.util.Log.e("FacebookCommentPoster", "Could not find original post text on screen")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Find the comment button associated with this post
|
||||||
|
// We go up to the parent container and look for a "Comment" or "تعليق" button
|
||||||
|
var commentBtn = findCommentButtonNearby(postNode)
|
||||||
|
|
||||||
|
if (commentBtn == null) {
|
||||||
|
// Fallback: just search the whole screen for the first comment button
|
||||||
|
commentBtn = findNodeByContentDescription(root, "Comment") ?: findNodeByContentDescription(root, "تعليق")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commentBtn != null) {
|
||||||
|
commentBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||||
|
delay(3000) // wait for comment overlay to open
|
||||||
|
|
||||||
|
val success = performCommentPosting(commentText)
|
||||||
|
|
||||||
|
// Go back to feed
|
||||||
|
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
|
||||||
|
delay(2000)
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.e("FacebookCommentPoster", "Could not find comment button")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun performCommentPosting(commentText: String): Boolean {
|
||||||
val root = service.rootInActiveWindow ?: return false
|
val root = service.rootInActiveWindow ?: return false
|
||||||
|
|
||||||
// 1. Find the comment input box.
|
|
||||||
// In Facebook, it might have contentDescription like "Write a comment..." or similar.
|
|
||||||
val inputNode = findNodeByContentDescription(root, "Write a comment")
|
val inputNode = findNodeByContentDescription(root, "Write a comment")
|
||||||
|
?: findNodeByContentDescription(root, "اكتب تعليق")
|
||||||
?: findNodeByClassName(root, "android.widget.EditText")
|
?: findNodeByClassName(root, "android.widget.EditText")
|
||||||
|
|
||||||
if (inputNode == null) return false
|
if (inputNode == null) return false
|
||||||
|
|
||||||
// 2. Click the input node to focus it
|
|
||||||
inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||||
delay(1000) // wait for keyboard
|
delay(1000)
|
||||||
|
|
||||||
// 3. Set text (simulating human typing can be done by pasting chunks or using set_text)
|
|
||||||
val arguments = Bundle()
|
val arguments = Bundle()
|
||||||
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText)
|
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText)
|
||||||
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
|
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
|
||||||
|
|
||||||
// Wait a bit to simulate human typing delay
|
val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(4000L)
|
||||||
val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(5000L)
|
|
||||||
delay(randomTypingDelay)
|
delay(randomTypingDelay)
|
||||||
|
|
||||||
// 4. Find and click the send button
|
|
||||||
// Often has a content description like "Send" or it's an ImageView next to EditText
|
|
||||||
val sendBtn = findNodeByContentDescription(service.rootInActiveWindow, "Send")
|
val sendBtn = findNodeByContentDescription(service.rootInActiveWindow, "Send")
|
||||||
|
?: findNodeByContentDescription(service.rootInActiveWindow, "إرسال")
|
||||||
|
|
||||||
if (sendBtn != null) {
|
if (sendBtn != null) {
|
||||||
sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||||
delay(2000)
|
delay(2000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,4 +108,33 @@ class FacebookCommentPoster(private val service: AccessibilityService) {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
private fun findNodeByExactText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
|
||||||
|
if (node == null) return null
|
||||||
|
|
||||||
|
val nodeText = node.text?.toString()?.trim() ?: node.contentDescription?.toString()?.trim()
|
||||||
|
if (nodeText == text.trim()) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until node.childCount) {
|
||||||
|
val child = findNodeByExactText(node.getChild(i), text)
|
||||||
|
if (child != null) return child
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCommentButtonNearby(postNode: AccessibilityNodeInfo): AccessibilityNodeInfo? {
|
||||||
|
// Go up the view hierarchy to find the container
|
||||||
|
var parent = postNode.parent
|
||||||
|
var depth = 0
|
||||||
|
while (parent != null && depth < 5) {
|
||||||
|
val btn = findNodeByContentDescription(parent, "Comment")
|
||||||
|
?: findNodeByContentDescription(parent, "تعليق")
|
||||||
|
if (btn != null) return btn
|
||||||
|
|
||||||
|
parent = parent.parent
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -50,7 +50,8 @@ class FacebookCommentReader(private val service: AccessibilityService) {
|
|||||||
"watch", "marketplace", "groups", "notifications", "menu",
|
"watch", "marketplace", "groups", "notifications", "menu",
|
||||||
"إعجاب", "تعليق", "مشاركة", "إرسال", "اكتب تعليقاً",
|
"إعجاب", "تعليق", "مشاركة", "إرسال", "اكتب تعليقاً",
|
||||||
"رد", "عرض المزيد من التعليقات", "الأكثر صلة", "الصفحة الرئيسية",
|
"رد", "عرض المزيد من التعليقات", "الأكثر صلة", "الصفحة الرئيسية",
|
||||||
"المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل"
|
"المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل",
|
||||||
|
"انتقال إلى الملف الشخصي", "اكتب تعليقًا...", "بحث في المجموعات"
|
||||||
)
|
)
|
||||||
|
|
||||||
for (ignore in ignoreList) {
|
for (ignore in ignoreList) {
|
||||||
@@ -59,6 +60,20 @@ class FacebookCommentReader(private val service: AccessibilityService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.5 Ignore UI patterns (Partial matches)
|
||||||
|
val ignorePatterns = listOf(
|
||||||
|
"علامة التبويب", "صورة ملف", "اقتراح", "لم تتم مشاهدتها",
|
||||||
|
"خيارات إضافية", "عرض المزيد من الخيارات", "تحديد صور أو مقاطع فيديو",
|
||||||
|
"إنشاء، اضغط ضغطًا مزدوجًا", "إزالة اقتراح", "عرض كل اقتراحات",
|
||||||
|
"زر أعجبني", "الزر مشاركة", "Action chip profile picture"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (pattern in ignorePatterns) {
|
||||||
|
if (trimmed.contains(pattern, ignoreCase = true)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Ignore texts that look like timestamps or metrics (e.g. "2 hrs", "10K Likes")
|
// 3. Ignore texts that look like timestamps or metrics (e.g. "2 hrs", "10K Likes")
|
||||||
// Simplistic check: if it's short and ends with common time units
|
// Simplistic check: if it's short and ends with common time units
|
||||||
if (trimmed.length < 25) {
|
if (trimmed.length < 25) {
|
||||||
|
|||||||
+63
@@ -23,4 +23,67 @@ class FacebookNavigator(private val service: AccessibilityService) {
|
|||||||
// Wait for app to open
|
// Wait for app to open
|
||||||
delay(3000)
|
delay(3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun openApp() {
|
||||||
|
val launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.katana")
|
||||||
|
if (launchIntent != null) {
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
service.startActivity(launchIntent)
|
||||||
|
delay(5000) // Wait 5 seconds for Facebook to load completely
|
||||||
|
|
||||||
|
// Try to navigate to Groups Tab automatically
|
||||||
|
openGroupsTab()
|
||||||
|
} else {
|
||||||
|
android.util.Log.e("FacebookNavigator", "Facebook app is not installed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun openGroupsTab() {
|
||||||
|
val root = service.rootInActiveWindow ?: return
|
||||||
|
|
||||||
|
// Search for Groups tab
|
||||||
|
val groupsTab = findNodeByContentDescription(root, "المجموعات")
|
||||||
|
?: findNodeByContentDescription(root, "Groups")
|
||||||
|
|
||||||
|
if (groupsTab != null) {
|
||||||
|
groupsTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
|
||||||
|
delay(3000) // wait for Groups tab to load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun scrollForward(): Boolean {
|
||||||
|
val root = service.rootInActiveWindow ?: return false
|
||||||
|
return performScroll(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performScroll(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
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until node.childCount) {
|
||||||
|
if (performScroll(node.getChild(i))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findNodeByContentDescription(node: android.view.accessibility.AccessibilityNodeInfo?, descPrefix: String): android.view.accessibility.AccessibilityNodeInfo? {
|
||||||
|
if (node == null) return null
|
||||||
|
|
||||||
|
if (node.contentDescription?.startsWith(descPrefix, ignoreCase = true) == true ||
|
||||||
|
node.text?.startsWith(descPrefix, ignoreCase = true) == true) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until node.childCount) {
|
||||||
|
val child = findNodeByContentDescription(node.getChild(i), descPrefix)
|
||||||
|
if (child != null) return child
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user