Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+64
View File
@@ -0,0 +1,64 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.siro.relay"
compileSdk = 36
defaultConfig {
applicationId = "com.siro.relay"
// ‏24 لا أعلى: الهاتف الذي يحمل الشريحة قد يكون جهازاً رخيصاً
// ‏قديماً مركوناً في المكتب، لا هاتفاً حديثاً.
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// ‏نفس مفتاح WEBHOOK_SECRET_KEY في backend — الويب‌هوك ونقطة
// ‏السحب كلاهما يتحقق منه. غيّره هنا وهناك معاً أو يتوقف كل شيء.
buildConfigField("String", "RELAY_SECRET", "\"CHANGE_ME_WEBHOOK_SECRET\"")
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
// ‏معرّف الجهاز: يظهر في `claimed_by` بجدول sms_outbox. لو شغّلت
// ‏هاتفين بشريحتين، غيّره في الثاني لتعرف أيّهما أرسل ماذا.
buildConfigField("String", "DEVICE_ID", "\"phone-1\"")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
debugImplementation(libs.androidx.compose.ui.tooling)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ‏استقبال الرسائل الواردة وإرسال الردود عبر شريحة الهاتف.
‏هاتان الصلاحيتان هما كل سبب وجود التطبيق. -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- ‏خدمة أمامية: بدونها يقتل النظام السحب الدوري خلال دقائق حين
‏تُطفأ الشاشة، فتتراكم الردود في الطابور ولا تصل. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- ‏الإقلاع مع الجهاز: انقطاع كهرباء يعيد تشغيل الهاتف، ولا أحد
‏بجانبه ليفتح التطبيق يدوياً. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SiroRelay"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- ‏أولوية عالية لا قصوى: نريد أن نصل قبل التطبيقات العادية،
‏لكن ليس أن ننازع تطبيق الرسائل الافتراضي على الأولوية. -->
<receiver
android:name=".SmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".RelayService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
@@ -0,0 +1,130 @@
package com.siro.relay
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
/**
* ‏عميل HTTP بسيط لنقطتي الباك إند.
*
* ‏HttpURLConnection لا OkHttp عمداً: التطبيق يقوم بنداءين اثنين لا أكثر،
* ‏وإضافة مكتبة شبكة كاملة من أجلهما تضخّم حزمةً ستعمل على هاتف قديم
* ‏مركون بلا صيانة. أقل ما يمكن كسره هو ما لم يُضَف أصلاً.
*/
object Api {
private const val TAG = "SiroRelay/Api"
private val WEBHOOK_URL = "${BuildConfig.BACKEND_HOST}/webhook_sms/webhook.php"
private val OUTBOX_URL = "${BuildConfig.BACKEND_HOST}/sms/outbox_pull.php"
/** رسالة صادرة كما وصلت من الطابور. */
data class Outgoing(val id: Int, val recipient: String, val body: String)
/**
* ‏يرفع رسالة واردة إلى الويب‌هوك.
*
* @return ‏true عند النجاح — المتصل يعيد المحاولة عند الفشل.
*/
fun pushIncoming(sender: String, message: String): Boolean {
val payload = JSONObject().apply {
put("sender", sender)
put("message", message)
}
return try {
val (code, _) = post(WEBHOOK_URL, payload.toString())
if (code != 200) Log.w(TAG, "الويب‌هوك رد بـ $code")
code == 200
} catch (e: Exception) {
Log.e(TAG, "تعذّر رفع الوارد: ${e.message}")
false
}
}
/** ‏يسحب دفعة رسائل للإرسال. قائمة فارغة تعني لا شيء ينتظر. */
fun pullOutgoing(): List<Outgoing> {
val payload = JSONObject().apply {
put("action", "pull")
put("device", BuildConfig.DEVICE_ID)
}
return try {
val (code, body) = post(OUTBOX_URL, payload.toString())
if (code != 200) {
Log.w(TAG, "السحب رد بـ $code")
return emptyList()
}
val arr = JSONObject(body).optJSONArray("messages") ?: return emptyList()
(0 until arr.length()).mapNotNull { i ->
val o = arr.optJSONObject(i) ?: return@mapNotNull null
val to = o.optString("recipient")
val text = o.optString("body")
// ‏صف ناقص لا يُرسل ولا يُسقط الدفعة: نتخطاه ويبقى في
// ‏الطابور حتى تنتهي مهلة الحجز فيُحرَّر ويُراجَع.
if (to.isBlank() || text.isBlank()) null
else Outgoing(o.optInt("id"), to, text)
}
} catch (e: Exception) {
Log.e(TAG, "تعذّر السحب: ${e.message}")
emptyList()
}
}
/**
* ‏يبلّغ الخادم بنتائج الإرسال.
*
* ‏التبليغ ليس تفصيلاً: الرسالة تبقى محجوزة في الخادم حتى يصله، وبلا
* ‏تبليغ تُحرَّر بعد المهلة وتُرسل ثانيةً — أي رسالتان للراكب نفسه.
*/
fun ack(results: List<Triple<Int, Boolean, String?>>): Boolean {
if (results.isEmpty()) return true
val arr = JSONArray()
results.forEach { (id, sent, error) ->
arr.put(JSONObject().apply {
put("id", id)
put("sent", sent)
if (error != null) put("error", error)
})
}
val payload = JSONObject().apply {
put("action", "ack")
put("device", BuildConfig.DEVICE_ID)
put("results", arr)
}
return try {
val (code, _) = post(OUTBOX_URL, payload.toString())
code == 200
} catch (e: Exception) {
Log.e(TAG, "تعذّر التبليغ: ${e.message}")
false
}
}
private fun post(url: String, json: String): Pair<Int, String> {
val conn = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
doOutput = true
connectTimeout = 10_000
readTimeout = 20_000
setRequestProperty("Content-Type", "application/json; charset=utf-8")
setRequestProperty("Authorization", "Bearer ${BuildConfig.RELAY_SECRET}")
}
return try {
conn.outputStream.use { it.write(json.toByteArray(Charsets.UTF_8)) }
val code = conn.responseCode
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
val body = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() } ?: ""
code to body
} finally {
conn.disconnect()
}
}
}
@@ -0,0 +1,20 @@
package com.siro.relay
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* ‏يعيد تشغيل الخدمة بعد إقلاع الجهاز.
*
* ‏ليس ترفاً: الهاتف الحامل للشريحة سيبقى مركوناً في المكتب، وانقطاع
* ‏كهرباء واحد يعيد تشغيله. بلا هذا المستقبِل تبقى القناة ميتة حتى
* ‏ينتبه أحد ويفتح التطبيق يدوياً — وقد لا ينتبه أحد لأيام.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
RelayService.start(context)
}
}
}
@@ -0,0 +1,98 @@
package com.siro.relay
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
/**
* ‏شاشة واحدة: تطلب الصلاحيات وتشغّل الخدمة.
*
* ‏لا لوحة إعدادات ولا سجل رسائل عمداً. هذا الجهاز يعمل بلا مشرف،
* ‏وكل زر إضافي فيه احتمال أن يعبث به أحد فيوقف القناة. الإعدادات
* ‏كلها في BuildConfig — تتغيّر ببناء جديد لا بلمسة على الشاشة.
*/
class MainActivity : ComponentActivity() {
private val required = buildList {
add(Manifest.permission.RECEIVE_SMS)
add(Manifest.permission.READ_SMS)
add(Manifest.permission.SEND_SMS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
}
private var granted by mutableStateOf(false)
private val requestPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
// ‏الإشعارات وحدها ليست حاسمة — رفضها يخفي الإشعار الدائم فقط.
// ‏أمّا صلاحيات الرسائل فبدونها التطبيق بلا وظيفة.
granted = result.filterKeys { it != Manifest.permission.POST_NOTIFICATIONS }
.all { it.value }
if (granted) RelayService.start(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
granted = required.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
if (granted) RelayService.start(this)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = getString(R.string.app_name),
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(12.dp))
Text(
text = getString(
if (granted) R.string.status_running else R.string.status_needs_permission
),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge
)
Spacer(Modifier.height(24.dp))
if (!granted) {
Button(onClick = { requestPermissions.launch(required.toTypedArray()) }) {
Text(getString(R.string.grant_permissions))
}
} else {
Text(
text = BuildConfig.BACKEND_HOST,
style = MaterialTheme.typography.bodySmall
)
Text(
text = "${getString(R.string.device_label)}: ${BuildConfig.DEVICE_ID}",
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}
}
@@ -0,0 +1,184 @@
package com.siro.relay
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.telephony.SmsManager
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
/**
* ‏خدمة أمامية تسحب الردود من الخادم وترسلها عبر شريحة الهاتف.
*
* ‏لماذا سحب لا دفع: الهاتف خلف شبكة الجوّال بلا عنوان ثابت ولا منفذ
* ‏مفتوح، فلا سبيل للخادم أن يبادره. والسحب يجعل انقطاع الهاتف حالة
* ‏طبيعية — تتراكم الرسائل في الطابور وتُرسل حين يعود، بلا فقدان.
*
* ‏ولماذا أمامية: النظام يقتل أي عمل دوري في الخلفية خلال دقائق بعد
* ‏إطفاء الشاشة. الإشعار الدائم ثمن مقبول مقابل قناة يُفترض ألّا تنام.
*/
class RelayService : Service() {
companion object {
private const val TAG = "SiroRelay/Service"
private const val CHANNEL_ID = "siro_relay"
private const val NOTIF_ID = 4210
private const val ACTION_POLL_NOW = "com.siro.relay.POLL_NOW"
/** ‏دورة السحب الاعتيادية. */
private const val POLL_INTERVAL_MS = 5_000L
/** ‏تباعد بين رسالتين متتاليتين — الشريحة لا تحب الدفعات السريعة. */
private const val SEND_GAP_MS = 1_500L
fun start(context: Context) {
val intent = Intent(context, RelayService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
/** ‏يوقظ دورة سحب فورية بدل انتظار الموقّت. */
fun requestImmediatePoll(context: Context) {
start(context)
wakeUp.set(true)
}
private val wakeUp = AtomicBoolean(false)
}
@Volatile private var running = false
private var worker: Thread? = null
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIF_ID, buildNotification())
// ‏استدعاء ثانٍ للخدمة العاملة لا يبدأ خيطاً ثانياً: خيطان يسحبان
// ‏معاً يعني رسائل تُرسل مرتين.
if (!running) {
running = true
worker = thread(name = "relay-loop") { loop() }
}
// ‏START_STICKY: لو قتلها النظام تحت ضغط ذاكرة، يعيدها. القناة
// ‏يجب أن تعود وحدها — لا أحد بجانب الهاتف ليفتح التطبيق.
return START_STICKY
}
override fun onDestroy() {
running = false
worker?.interrupt()
super.onDestroy()
}
private fun loop() {
while (running) {
try {
pumpOnce()
} catch (e: Exception) {
Log.e(TAG, "خطأ في الدورة: ${e.message}")
}
// ‏نوم مجزّأ ليستجيب للإيقاظ الفوري بدل انتظار الدورة كاملة.
var slept = 0L
while (running && slept < POLL_INTERVAL_MS) {
if (wakeUp.compareAndSet(true, false)) break
try {
Thread.sleep(250)
} catch (e: InterruptedException) {
return
}
slept += 250
}
}
}
private fun pumpOnce() {
val batch = Api.pullOutgoing()
if (batch.isEmpty()) return
Log.i(TAG, "سُحبت ${batch.size} رسالة")
val results = mutableListOf<Triple<Int, Boolean, String?>>()
for (msg in batch) {
val error = sendSms(msg.recipient, msg.body)
results.add(Triple(msg.id, error == null, error))
if (msg !== batch.last()) {
try {
Thread.sleep(SEND_GAP_MS)
} catch (e: InterruptedException) {
break
}
}
}
// ‏نبلّغ عن كل ما أُرسل فعلاً حتى لو انقطعت الدفعة في منتصفها،
// ‏وإلا أُعيد إرسال ما نجح منها بعد انتهاء مهلة الحجز.
Api.ack(results)
}
/** @return ‏رسالة الخطأ، أو null عند النجاح. */
private fun sendSms(to: String, body: String): String? {
return try {
val manager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getSystemService(SmsManager::class.java)
} else {
@Suppress("DEPRECATION")
SmsManager.getDefault()
} ?: return "SmsManager unavailable"
// ‏الرسالة العربية تتجاوز حد الجزء الواحد بسرعة (٧٠ حرفاً في
// ‏ترميز UCS-2 لا ١٦٠). التقسيم إجباري لا اختياري.
val parts = manager.divideMessage(body)
if (parts.size > 1) {
manager.sendMultipartTextMessage(to, null, parts, null, null)
} else {
manager.sendTextMessage(to, null, body, null, null)
}
null
} catch (e: Exception) {
Log.e(TAG, "تعذّر الإرسال إلى $to: ${e.message}")
e.message ?: "send failed"
}
}
private fun buildNotification(): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.channel_name),
// ‏أدنى أهمية: إشعار دائم لا ينبغي أن يصدر صوتاً ولا أن
// ‏يقفز فوق الشاشة كل مرة.
NotificationManager.IMPORTANCE_MIN
)
getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
}
val tap = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.notif_title))
.setContentText(getString(R.string.notif_text))
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setOngoing(true)
.setContentIntent(tap)
.build()
}
}
@@ -0,0 +1,49 @@
package com.siro.relay
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.provider.Telephony
import android.util.Log
import kotlin.concurrent.thread
/**
* ‏يلتقط كل رسالة واردة ويرفعها إلى الويب‌هوك.
*
* ‏لا فرز هنا إطلاقاً: التطبيق يرفع كل شيء، والخادم يقرّر أهي طلب رحلة
* ‏أم إشعار دفع أم شيء ثالث. نقل الفرز إلى الهاتف كان سيعني أن كل تعديل
* ‏على قواعد الفهم يستوجب تحديث تطبيق على جهاز لا أحد يقترب منه.
*/
class SmsReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return
val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) ?: return
if (messages.isEmpty()) return
// ‏الرسالة الطويلة تصل مقسّمة إلى أجزاء متتالية من نفس المرسِل.
// ‏رفع كل جزء وحده كان سينتج طلبات مبتورة بلا معنى — فنلمّها
// ‏بالمرسِل ونرفع النص كاملاً.
val bySender = messages
.groupBy { it.displayOriginatingAddress ?: it.originatingAddress ?: "" }
.mapValues { (_, parts) -> parts.joinToString("") { it.displayMessageBody ?: "" } }
// ‏onReceive يعمل على الخيط الرئيسي وله ثوانٍ معدودة قبل أن يقتله
// ‏النظام. النداء الشبكي في خيط منفصل، ونتحمّل أن يُفقد لو مات
// ‏السياق — المرسِل سيعيد الإرسال حين لا يصله رد.
bySender.forEach { (sender, body) ->
if (sender.isBlank() || body.isBlank()) return@forEach
thread(name = "relay-in") {
val ok = Api.pushIncoming(sender, body)
Log.i("SiroRelay", if (ok) "رُفعت رسالة من $sender" else "فشل رفع رسالة من $sender")
// ‏نوقظ خدمة السحب فوراً بدل انتظار الدورة التالية:
// ‏الرد على الراكب جاهز في الطابور خلال أجزاء من الثانية
// ‏بعد الرفع، وانتظاره دورةً كاملة تأخير بلا سبب.
if (ok) RelayService.requestImmediatePoll(context)
}
}
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,10 @@
<resources>
<string name="app_name">وسيط سيرو</string>
<string name="channel_name">قناة الرسائل</string>
<string name="notif_title">وسيط سيرو يعمل</string>
<string name="notif_text">يستقبل طلبات الركاب ويرسل الردود</string>
<string name="status_running">القناة تعمل. اترك الهاتف موصولاً بالكهرباء والشبكة.</string>
<string name="status_needs_permission">يحتاج التطبيق صلاحيات الرسائل ليعمل.</string>
<string name="grant_permissions">منح الصلاحيات</string>
<string name="device_label">معرّف الجهاز</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SiroRelay" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
‏HTTPS فقط، بلا أي استثناء.
‏نسخة android_bot تسمح بنص صريح لعدة نطاقات. هنا لا يجوز: ما يمرّ في
‏هذه القناة أرقام هواتف ركّاب وعناوين انطلاق ووجهات، ومفتاح Bearer
‏يمنح حامله حق ضخّ الرسائل. الهاتف سيعمل على شبكة جوّال عامة سنوات
‏بلا مراجعة — الافتراض الآمن هو الافتراض الوحيد المقبول.
-->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>