2026-02-16
This commit is contained in:
@@ -14,6 +14,8 @@ import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import java.io.IOException
|
||||
import java.util.ArrayList
|
||||
import java.util.HashSet
|
||||
import java.util.Random
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
@@ -27,116 +29,156 @@ class ShamAccessibilityService : AccessibilityService() {
|
||||
private val client = OkHttpClient()
|
||||
private val processedIds = HashSet<String>()
|
||||
|
||||
// إعدادات التحديث التلقائي
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val random = Random()
|
||||
private val minRefreshTime = 70000L // 100 ثانية
|
||||
private val maxRefreshTime = 140000L // 3 دقائق
|
||||
// عدل هذين السطرين مؤقتاً للتجربة
|
||||
// private val minRefreshTime = 10000L // 10 ثواني
|
||||
// private val maxRefreshTime = 15000L // 15 ثانية
|
||||
private val minRefreshTime = 70000L
|
||||
private val maxRefreshTime = 140000L
|
||||
|
||||
// إعدادات الإشعارات
|
||||
private val CHANNEL_ID = "ShamBotChannel"
|
||||
private val NOTIFICATION_ID_REFRESH = 1
|
||||
private var NOTIFICATION_ID_TRANSACTION = 2
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
Log.d("ShamBot", "✅ Service Connected.")
|
||||
Log.d("ShamBot", "✅ Service Connected. V15 Direct Line Active!")
|
||||
createNotificationChannel()
|
||||
|
||||
// محاولة إظهار إشعار ترحيبي للتأكد من القناة
|
||||
showNotification("تم تفعيل البوت", "البوت جاهز للعمل 🤖", NOTIFICATION_ID_REFRESH, false)
|
||||
|
||||
showNotification(
|
||||
"تم تفعيل البوت",
|
||||
"V15: القراءة المباشرة للأسطر 📜",
|
||||
NOTIFICATION_ID_REFRESH,
|
||||
false
|
||||
)
|
||||
scheduleNextRefresh()
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
val rootNode = rootInActiveWindow ?: return
|
||||
findTransactions(rootNode)
|
||||
|
||||
// تجميع وتفتيت النصوص (Splitter)
|
||||
val allScreenLines = ArrayList<String>()
|
||||
collectAndSplitScreenText(rootNode, allScreenLines)
|
||||
|
||||
// التحليل
|
||||
processScreenContent(allScreenLines)
|
||||
}
|
||||
|
||||
private fun findTransactions(node: AccessibilityNodeInfo?) {
|
||||
// 🛠️ دالة التفتيت (Splitter)
|
||||
private fun collectAndSplitScreenText(node: AccessibilityNodeInfo?, list: MutableList<String>) {
|
||||
node ?: return
|
||||
|
||||
val text = node.text?.toString()
|
||||
val contentDesc = node.contentDescription?.toString()
|
||||
val fullData = contentDesc ?: text
|
||||
val rawData = contentDesc ?: text
|
||||
|
||||
if (fullData != null &&
|
||||
fullData.contains("#") &&
|
||||
(fullData.contains("ل.س") || fullData.contains("SYP"))
|
||||
) {
|
||||
val transaction = parseData(fullData)
|
||||
|
||||
if (transaction != null) {
|
||||
val txId = transaction.optString("id")
|
||||
val amount = transaction.optString("amount")
|
||||
|
||||
if (!processedIds.contains(txId)) {
|
||||
Log.d("ShamBot", "🚀 New Transaction: $txId")
|
||||
processedIds.add(txId)
|
||||
sendToServer(transaction)
|
||||
|
||||
showNotification(
|
||||
"💰 وصلتك حوالة!",
|
||||
"$amount ل.س - رقم: $txId",
|
||||
NOTIFICATION_ID_TRANSACTION++,
|
||||
true
|
||||
)
|
||||
|
||||
if (processedIds.size > 100) processedIds.clear()
|
||||
if (!rawData.isNullOrEmpty()) {
|
||||
// تقسيم النص إلى أسطر
|
||||
val lines = rawData.split("\n")
|
||||
for (line in lines) {
|
||||
if (line.trim().isNotEmpty()) {
|
||||
list.add(line.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until node.childCount) {
|
||||
findTransactions(node.getChild(i))
|
||||
collectAndSplitScreenText(node.getChild(i), list)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseData(rawData: String): JSONObject? {
|
||||
try {
|
||||
val lines = rawData.split("\n").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
var txId = ""
|
||||
// 🔥 دالة التحليل (V15 - Direct Line Logic)
|
||||
private fun processScreenContent(lines: List<String>) {
|
||||
var i = 0
|
||||
while (i < lines.size) {
|
||||
val currentLine = lines[i].trim()
|
||||
|
||||
// 1. استخراج الـ ID
|
||||
val idMatch = Regex("#([0-9]+)").find(currentLine)
|
||||
|
||||
if (idMatch != null) {
|
||||
val txId = idMatch.groupValues[1]
|
||||
|
||||
// نتأكد أن الـ ID صحيح
|
||||
if (txId.length in 5..12 && !processedIds.contains(txId)) {
|
||||
|
||||
var amount = ""
|
||||
var username = "Unknown"
|
||||
var note = ""
|
||||
|
||||
if (lines.isNotEmpty() && !lines[0].contains("#")) username = lines[0]
|
||||
// 2. استخراج المبلغ (السطر التالي مباشرة) 💰
|
||||
// حسب الـ Dump، المبلغ يأتي دائماً في السطر التالي للـ ID
|
||||
if (i + 1 < lines.size) {
|
||||
val nextLine = lines[i + 1].trim()
|
||||
// تنظيف السطر من أي شيء ليس رقماً
|
||||
val potentialAmount = nextLine.replace(Regex("[^0-9.]"), "")
|
||||
|
||||
lines.forEachIndexed { index, line ->
|
||||
if (line.contains("#")) txId = line.replace(Regex("[^0-9]"), "")
|
||||
if (line.contains("ل.س") || line.contains("SYP")) {
|
||||
if (index > 0) amount = lines[index - 1].replace(Regex("[^0-9.]"), "")
|
||||
// إذا كان السطر يحتوي على أرقام، فهو المبلغ
|
||||
if (potentialAmount.isNotEmpty()) {
|
||||
amount = potentialAmount
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.size > 2) {
|
||||
val lastLine = lines.last()
|
||||
val isNotDate =
|
||||
!lastLine.contains("2025") &&
|
||||
!lastLine.contains("/") &&
|
||||
!lastLine.contains(":")
|
||||
val isNotId = !lastLine.contains("#")
|
||||
val isNotCurrency = !lastLine.contains("ل.س")
|
||||
if (isNotDate && isNotId && isNotCurrency) note = lastLine
|
||||
// 3. استخراج الملاحظة (البحث عن رقم سداسي في الأسطر التالية) 🎯
|
||||
// نبحث في الـ 6 أسطر التالية
|
||||
for (k in 1..6) {
|
||||
if (i + k < lines.size) {
|
||||
val line = lines[i + k].trim()
|
||||
val cleanDigits = line.replace(Regex("[^0-9]"), "")
|
||||
|
||||
// شروط الملاحظة:
|
||||
// 1. رقم سداسي (6 خانات)
|
||||
// 2. السطر كله رقم (أو أغلبه)
|
||||
// 3. ليس هو الـ ID وليس هو المبلغ
|
||||
// 4. لا يبدأ بـ 202 (ليس سنة)
|
||||
|
||||
if (cleanDigits.length == 6 &&
|
||||
line.matches(Regex("^[0-9]+$")) &&
|
||||
cleanDigits != txId &&
|
||||
!cleanDigits.startsWith("202")
|
||||
) {
|
||||
note = cleanDigits
|
||||
break // وجدنا الملاحظة
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (txId.length > 4 && amount.isNotEmpty()) {
|
||||
// 4. الإرسال
|
||||
// نرسل إذا وجدنا المبلغ
|
||||
if (amount.isNotEmpty()) {
|
||||
val amountClean = amount.replace(".", "")
|
||||
|
||||
// تجهيز الملاحظة للإرسال
|
||||
val logNote = if (note.isNotEmpty()) note else "" // نرسلها فارغة لو لم توجد
|
||||
|
||||
Log.d(
|
||||
"ShamBot",
|
||||
"🚀 CAPTURED -> ID:$txId | Amt:$amountClean | Note:$logNote"
|
||||
)
|
||||
|
||||
val json = JSONObject()
|
||||
json.put("id", txId)
|
||||
json.put("username", username)
|
||||
json.put("amount", amount)
|
||||
json.put("note", note)
|
||||
json.put("source", "android_bot_v5_swipe_fix")
|
||||
return json
|
||||
json.put("username", "User")
|
||||
json.put("amount", amountClean)
|
||||
json.put("note", logNote)
|
||||
json.put("source", "android_v15_direct_line")
|
||||
|
||||
processedIds.add(txId)
|
||||
sendToServer(json)
|
||||
|
||||
val notifText =
|
||||
if (logNote.isNotEmpty()) "$amountClean (#$txId) Inv:$logNote"
|
||||
else "$amountClean (#$txId)"
|
||||
showNotification(
|
||||
"💰 عملية جديدة",
|
||||
notifText,
|
||||
NOTIFICATION_ID_TRANSACTION++,
|
||||
true
|
||||
)
|
||||
|
||||
if (processedIds.size > 200) processedIds.clear()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShamBot", "Parsing Error", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendToServer(json: JSONObject) {
|
||||
@@ -151,8 +193,8 @@ class ShamAccessibilityService : AccessibilityService() {
|
||||
Log.e("ShamBot", "Network Error", e)
|
||||
processedIds.remove(json.optString("id"))
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
// طباعة رد السيرفر للتأكد
|
||||
Log.d("ShamBot", "✅ Server Response: ${response.code}")
|
||||
response.close()
|
||||
}
|
||||
@@ -160,48 +202,34 @@ class ShamAccessibilityService : AccessibilityService() {
|
||||
)
|
||||
}
|
||||
|
||||
// --- 🔔 Notifications Setup ---
|
||||
// --- بقية الكود (Notifications & Refresh) ---
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = "Sham Bot Alerts"
|
||||
val descriptionText = "Show transactions and status"
|
||||
val importance = NotificationManager.IMPORTANCE_HIGH // رفعنا الأهمية لتظهر فوق
|
||||
val channel =
|
||||
NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||
description = descriptionText
|
||||
}
|
||||
val notificationManager: NotificationManager =
|
||||
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||
val channel = NotificationChannel(CHANNEL_ID, name, importance)
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotification(title: String, content: String, id: Int, sound: Boolean) {
|
||||
// التأكد من أن الإشعار سيظهر حتى لو كان صامتاً
|
||||
val builder =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done) // أيقونة أوضح
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH) // أولوية قصوى
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
|
||||
if (!sound) {
|
||||
builder.setSound(null)
|
||||
builder.setVibrate(longArrayOf(0L))
|
||||
} else {
|
||||
builder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
}
|
||||
|
||||
if (!sound) builder.setSound(null)
|
||||
val notificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(id, builder.build())
|
||||
}
|
||||
|
||||
// --- 🤖 AUTO REFRESH LOGIC (V6 - The Heavy Pull Fix) ---
|
||||
private fun scheduleNextRefresh() {
|
||||
val delay = minRefreshTime + random.nextInt((maxRefreshTime - minRefreshTime).toInt())
|
||||
Log.d("ShamBot", "⏳ Next refresh in ${delay / 1000} seconds")
|
||||
handler.postDelayed(
|
||||
{
|
||||
performSwipeRefresh()
|
||||
@@ -212,53 +240,22 @@ class ShamAccessibilityService : AccessibilityService() {
|
||||
}
|
||||
|
||||
private fun performSwipeRefresh() {
|
||||
Log.d("ShamBot", "🔄 Refreshing (ULTRA SLOW & LONG Swipe)...")
|
||||
// لا داعي للإشعار الصامت هنا لأنه يظهر ويختفي بسرعة وقد يربك المستخدم، نكتفي باللوج والسحب
|
||||
// الفعلي
|
||||
// showNotification("🔄 جاري التحديث", "سحب قوي...", NOTIFICATION_ID_REFRESH, false)
|
||||
|
||||
Log.d("ShamBot", "🔄 Refreshing...")
|
||||
val displayMetrics = resources.displayMetrics
|
||||
// استخدام toFloat() لضمان دقة الحسابات
|
||||
val screenHeight = displayMetrics.heightPixels.toFloat()
|
||||
val screenWidth = displayMetrics.widthPixels.toFloat()
|
||||
|
||||
val centerX = screenWidth / 2
|
||||
|
||||
// 🛠️ التعديل الجذري V6: سحب أبطأ وأعمق
|
||||
// نبدأ من ربع الشاشة (لضمان أننا تحت الـ App Bar تماماً)
|
||||
val startY = screenHeight * 0.25f
|
||||
// ننزل إلى ما قبل القاع بقليل (سحبة طويلة جداً)
|
||||
val endY = screenHeight * 0.90f
|
||||
|
||||
val path = Path()
|
||||
path.moveTo(centerX, startY)
|
||||
path.lineTo(centerX, endY)
|
||||
|
||||
// 💡 السر هنا: زيادة المدة الزمنية بشكل كبير.
|
||||
// 500ms سريعة جداً وتعتبر "نقر" أو "Fling".
|
||||
// 1500ms (ثانية ونصف) تعتبر سحباً "ثقيلاً" ومتعمداً يفهمه التطبيق كتحديث.
|
||||
val duration = 1500L // ثانية ونصف
|
||||
|
||||
val centerX = displayMetrics.widthPixels.toFloat() / 2
|
||||
val startY = screenHeight * 0.20f
|
||||
val endY = screenHeight * 0.85f
|
||||
val path =
|
||||
Path().apply {
|
||||
moveTo(centerX, startY)
|
||||
lineTo(centerX, endY)
|
||||
}
|
||||
val gesture =
|
||||
GestureDescription.Builder()
|
||||
.addStroke(GestureDescription.StrokeDescription(path, 0, duration))
|
||||
.addStroke(GestureDescription.StrokeDescription(path, 0, 1500L))
|
||||
.build()
|
||||
|
||||
dispatchGesture(
|
||||
gesture,
|
||||
object : GestureResultCallback() {
|
||||
override fun onCompleted(gestureDescription: GestureDescription?) {
|
||||
Log.d("ShamBot", "✅ Ultra Swipe Sent Successfully")
|
||||
}
|
||||
override fun onCancelled(gestureDescription: GestureDescription?) {
|
||||
Log.d(
|
||||
"ShamBot",
|
||||
"⚠️ Ultra Swipe Cancelled (App might be obscuring the view)"
|
||||
)
|
||||
}
|
||||
},
|
||||
null
|
||||
)
|
||||
dispatchGesture(gesture, null, null)
|
||||
}
|
||||
|
||||
override fun onInterrupt() {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/accessibility_service_description"
|
||||
android:packageNames="com.shmacash.shamcash"
|
||||
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
|
||||
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds"
|
||||
android:accessibilityEventTypes="typeAllMask"
|
||||
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:notificationTimeout="100"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canPerformGestures="true"
|
||||
android:notificationTimeout="100" />
|
||||
android:canPerformGestures="true" />
|
||||
Reference in New Issue
Block a user