feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,139 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.siro.siro_rider">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<!-- Package visibility declarations for external apps like maps and WhatsApp -->
<queries>
<package android:name="com.whatsapp" />
<package android:name="com.google.android.apps.maps" />
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
</queries>
<application
android:name="${applicationName}"
android:label="@string/label"
android:icon="@mipmap/launcher_icon"
android:allowBackup="false"
android:fullBackupContent="false"
android:usesCleartextTraffic="false"
android:networkSecurityConfig="@xml/network_security_config">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="siromove.com" android:pathPrefix="/" />
<data android:scheme="https" android:host="www.siromove.com"
android:pathPrefix="/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="siromove" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geo" />
</intent-filter>
<!-- Navigation Intents -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="google.navigation" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/map" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${mapsApiKey}" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
</application>
</manifest>
@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.10.2) # Use 3.10.2 for consistency.
project(siro)
add_library(native-lib SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
@@ -0,0 +1,192 @@
#include <jni.h>
#include <string>
#include <fstream>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <android/log.h>
#include <pthread.h>
#include <dlfcn.h>
#include <link.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ptrace.h> // Add this line
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// Function to check for common root binaries (including Magisk, KernelSU, APatch)
bool isRooted()
{
std::string paths[] = {
"/system/app/Superuser.apk",
"/system/xbin/su",
"/system/bin/su",
"/system/bin/magisk",
"/system/xbin/magisk",
"/sbin/magisk",
"/data/adb/magisk/magiskinit",
"/data/adb/magisk/magisk",
"/data/adb/magisk.db",
"/data/adb/ksu",
"/data/adb/apatch",
"/data/adb/ap/single"};
for (const auto &path : paths)
{
std::ifstream file(path);
if (file.good())
{
return true;
}
}
return false;
}
// Function to check for the presence of files or directories commonly associated with Frida.
bool checkFridaFiles()
{
std::string fridaFiles[] = {
"/data/local/tmp/re.frida.server", // Common Frida server path
"/data/local/tmp/frida-server",
"/usr/lib/libfrida-gadget.so", // Frida gadget (injected library)
"/usr/lib64/libfrida-gadget.so",
"/data/local/re.frida.server",
};
for (const auto &path : fridaFiles)
{
if (access(path.c_str(), F_OK) != -1)
{
LOGE("Frida file detected: %s", path.c_str());
return true;
}
}
return false;
}
// Checks for open ports commonly used by Frida. This is less reliable, as ports can be changed.
bool checkFridaPorts()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
return false; // Couldn't create socket, not a strong indicator.
}
sockaddr_in sa;
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(27042); // Default Frida port
inet_pton(AF_INET, "127.0.0.1", &sa.sin_addr);
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) != -1)
{
LOGE("Frida default port (27042) is open.");
close(sock);
return true;
}
close(sock);
return false;
}
// Check the maps file of the current process for any suspicious entries.
bool checkMaps()
{
std::ifstream mapsFile("/proc/self/maps");
std::string line;
if (mapsFile.is_open())
{
while (std::getline(mapsFile, line))
{
// Look for lines that indicate injected libraries, especially Frida.
if (line.find("frida") != std::string::npos ||
line.find("gum-js-") != std::string::npos)
{ // Gum is Frida's JavaScript engine
LOGE("Suspicious entry in /proc/self/maps: %s", line.c_str());
return true;
}
}
mapsFile.close();
}
else
{
LOGE("Could not open /proc/self/maps");
return false;
}
return false;
}
// Check loaded modules.
bool checkLoadedModules()
{
bool found = false;
dl_iterate_phdr([](struct dl_phdr_info *info, size_t size, void *data)
{
bool *found_ptr = static_cast<bool *>(data);
if (std::string(info->dlpi_name).find("frida") != std::string::npos)
{
LOGE("Frida module detected: %s", info->dlpi_name);
*found_ptr = true;
return 1; // Stop iterating
}
return 0; // Continue iterating
},
&found);
return found;
}
// This is a simple ptrace check. More sophisticated checks are possible (and necessary for robust detection).
// bool checkPtrace() {
// // Attempt to ptrace ourselves. If another process is already tracing us, this will fail.
// if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
// LOGE("ptrace failed. Debugger or tracer detected.");
// return true; // Likely being traced
// }
// // Detach. If attached, need to detach to not interfere.
// ptrace(PTRACE_DETACH, 0, 0, 0);
// return false;
//}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_siro_siro_1rider_RootDetection_isNativeRooted(JNIEnv *env, jobject /* this */)
{
if (isRooted())
{
return JNI_TRUE;
}
if (checkFridaFiles())
{
return JNI_TRUE;
}
if (checkFridaPorts())
{
return JNI_TRUE;
}
if (checkMaps())
{
return JNI_TRUE;
}
if (checkLoadedModules())
{
return JNI_TRUE;
}
// if (checkPtrace()) {
// return JNI_TRUE;
// }
return JNI_FALSE;
}
@@ -0,0 +1,328 @@
package com.siro.siro_rider
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
/**
* خدمة تحديث حالة الرحلة في شريط الإشعارات على أندرويد.
*
* تعمل على ثلاثة مستويات حسب الجهاز:
* - أندرويد 16 فما فوق (API 36+) مع موافقة المستخدم: تُستخدم
* ميزة Live Updates الحقيقية عبر Notification.ProgressStyle
* (نفس الفئة والدوال التي تحققنا منها مباشرة من ملف الـSDK).
* - أندرويد 16 فما فوق بدون موافقة المستخدم، أو أي إصدار من
* API 23 حتى 35: إشعار عادٍ ثابت (Ongoing) بشريط تقدّم كلاسيكي،
* يعمل بشكل متطابق تقريباً بصرياً لكن بدون معاملة النظام الخاصة.
* - أقل من API 26: لا يتم إنشاء قناة إشعارات (الكلاس غير موجود
* أصلاً في النظام قبل هذا الإصدار)، ويُستخدم مسار الإشعار
* الكلاسيكي مباشرة بدون قناة.
*/
class AndroidLiveUpdatePlugin(private val context: Context) {
companion object {
private const val CHANNEL_ID = "siro_live_ride_progress"
private const val CHANNEL_NAME = "Siro Live Ride Progress"
private const val CHANNEL_DESC = "Live progress updates for your current Siro ride"
private const val NOTIFICATION_ID = 999
private const val STATUS_SEARCHING = "searching"
private const val STATUS_DRIVER_ON_WAY = "driver_on_way"
private const val STATUS_DRIVER_ARRIVED = "driver_arrived"
private const val STATUS_IN_PROGRESS = "in_progress"
private const val STATUS_FINISHED = "finished"
}
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private var currentProgress: Int = 0
private var currentMaxProgress: Int = 100
private var currentStatus: String = STATUS_SEARCHING
private var currentTitle: String = ""
private var currentBody: String = ""
init {
// NotificationChannel نفسها غير موجودة في النظام قبل API 26،
// فاستدعاؤها بدون هذا الشرط يتسبب بانهيار فوري للتطبيق عند
// الإقلاع على أي جهاز أقدم (Android 6/7).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = CHANNEL_DESC
setShowBadge(false)
enableVibration(false)
enableLights(false)
}
notificationManager.createNotificationChannel(channel)
}
/**
* هل يمكن فعلياً استخدام Live Updates الحقيقية الآن؟
* تتطلب أندرويد 16+ وموافقة صريحة من المستخدم عبر إعدادات النظام
* (Settings > Apps > Siro > Notifications > Live Updates).
*/
@Suppress("NewApi")
private fun canUsePromotedLiveUpdate(): Boolean {
if (Build.VERSION.SDK_INT < 36) return false
return try {
notificationManager.canPostPromotedNotifications()
} catch (e: Exception) {
false
}
}
private fun statusColor(): Int = when (currentStatus) {
STATUS_DRIVER_ARRIVED -> Color.parseColor("#22C55E")
STATUS_IN_PROGRESS -> Color.parseColor("#1A2340")
STATUS_SEARCHING -> Color.parseColor("#F59E0B")
else -> Color.parseColor("#1A2340")
}
private fun buildContentIntent(): PendingIntent {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("notification_ride", true)
}
return PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
/**
* بدء التحديث المباشر للرحلة
*/
fun startLiveUpdate(
rideId: String,
driverName: String,
etaText: String,
progress: Double,
carDetails: String
) {
currentStatus = STATUS_DRIVER_ON_WAY
currentTitle = "🚗 السائق في الطريق إليك"
currentBody = "$driverName • $carDetails • $etaText"
currentProgress = (progress * 100).toInt().coerceIn(0, 100)
currentMaxProgress = 100
render(indeterminate = false)
}
/**
* تحديث التقدم والوقت المتبقي
*/
fun updateProgress(progress: Double, etaText: String) {
currentProgress = (progress * 100).toInt().coerceIn(0, 100)
currentBody = when (currentStatus) {
STATUS_DRIVER_ON_WAY -> {
val parts = currentBody.split(" • ")
if (parts.size >= 3) {
"${parts[0]} • ${parts[1]} • $etaText"
} else {
"$currentBody • $etaText"
}
}
STATUS_IN_PROGRESS -> "المتبقي تقريبًا: $etaText"
else -> currentBody
}
render(indeterminate = false)
}
/**
* تحديث حالة الرحلة بالكامل
*/
fun updateStatus(
status: String,
driverName: String,
etaText: String,
progress: Double,
carDetails: String
) {
currentStatus = status
currentProgress = (progress * 100).toInt().coerceIn(0, 100)
when (status) {
STATUS_SEARCHING -> {
currentTitle = "🔍 جاري البحث عن سائق"
currentBody = etaText
render(indeterminate = true)
}
STATUS_DRIVER_ON_WAY -> {
currentTitle = "🚗 السائق في الطريق إليك"
currentBody = "$driverName • $carDetails • $etaText"
render(indeterminate = false)
}
STATUS_DRIVER_ARRIVED -> {
currentTitle = "📍 السائق وصل"
currentBody = "الرجاء التوجّه لمقابلة $driverName عند نقطة الالتقاء"
currentProgress = 100
render(indeterminate = false)
}
STATUS_IN_PROGRESS -> {
currentTitle = "🚀 الرحلة جارية الآن"
currentBody = "المتبقي تقريبًا: $etaText"
render(indeterminate = false)
}
}
}
/**
* إنهاء التحديث المباشر وإزالة الإشعار
*/
fun endLiveUpdate() {
notificationManager.cancel(NOTIFICATION_ID)
currentStatus = STATUS_FINISHED
currentProgress = 0
}
/**
* يختار المسار المناسب: Live Update الحقيقية إن كانت متاحة ومسموحة،
* وإلا الإشعار الكلاسيكي الآمن على كل الإصدارات من API 23.
*/
private fun render(indeterminate: Boolean) {
if (canUsePromotedLiveUpdate()) {
try {
showPromotedProgressNotification(indeterminate)
return
} catch (e: Exception) {
// أي فشل غير متوقع بالمسار الجديد يرجعنا فوراً للمسار الآمن
}
}
showClassicNotification(indeterminate)
}
/**
* المسار الجديد: Notification.ProgressStyle الحقيقية (Android 16 / API 36+).
* التوقيعات هنا مؤكدة مباشرة من ملف android-36/android.jar (javap)، وليست تخميناً.
*/
@RequiresApi(36)
private fun showPromotedProgressNotification(indeterminate: Boolean) {
val trackerIcon = Icon.createWithResource(context, android.R.drawable.ic_menu_directions)
val progressStyle = Notification.ProgressStyle()
.setProgressSegments(listOf(
Notification.ProgressStyle.Segment(100).setColor(statusColor())
))
.setProgress(currentProgress)
.setProgressIndeterminate(indeterminate)
.setProgressTrackerIcon(trackerIcon)
.setStyledByProgress(true)
val notification = Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(currentTitle)
.setContentText(currentBody)
.setStyle(progressStyle)
.setCategory(Notification.CATEGORY_PROGRESS)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(buildContentIntent())
.setFlag(Notification.FLAG_PROMOTED_ONGOING, true)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
/**
* المسار الكلاسيكي: إشعار ثابت بشريط تقدّم عادي، متوافق من API 23 وما فوق
* (وبدون قناة إشعارات على الإصدارات الأقدم من API 26).
*/
private fun showClassicNotification(indeterminate: Boolean) {
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
android.R.drawable.ic_menu_directions
)
)
.setContentTitle(currentTitle)
.setContentText(currentBody)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(currentBody)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setAutoCancel(false)
.setOnlyAlertOnce(true)
.setContentIntent(buildContentIntent())
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (indeterminate) {
notificationBuilder.setProgress(0, 0, true)
} else {
notificationBuilder.setProgress(currentMaxProgress, currentProgress, false)
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
/**
* معالجة استدعاءات MethodChannel
*/
fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"startLiveUpdate" -> {
val rideId = call.argument<String>("rideId") ?: ""
val driverName = call.argument<String>("driverName") ?: ""
val etaText = call.argument<String>("etaText") ?: ""
val progress = call.argument<Double>("progress") ?: 0.0
val carDetails = call.argument<String>("carDetails") ?: ""
startLiveUpdate(rideId, driverName, etaText, progress, carDetails)
result.success(true)
}
"updateProgress" -> {
val progress = call.argument<Double>("progress") ?: 0.0
val etaText = call.argument<String>("etaText") ?: ""
updateProgress(progress, etaText)
result.success(true)
}
"updateStatus" -> {
val status = call.argument<String>("status") ?: ""
val driverName = call.argument<String>("driverName") ?: ""
val etaText = call.argument<String>("etaText") ?: ""
val progress = call.argument<Double>("progress") ?: 0.0
val carDetails = call.argument<String>("carDetails") ?: ""
updateStatus(status, driverName, etaText, progress, carDetails)
result.success(true)
}
"endLiveUpdate" -> {
endLiveUpdate()
result.success(true)
}
"isSupported" -> {
// الإشعار الكلاسيكي يعمل من API 23 وما فوق؛ الأجهزة الأحدث
// تحصل تلقائياً على Live Update الحقيقية عبر canUsePromotedLiveUpdate().
result.success(Build.VERSION.SDK_INT >= 23)
}
else -> {
result.notImplemented()
}
}
}
}
@@ -0,0 +1,257 @@
package com.siro.siro_rider
import android.app.AlertDialog
import android.app.PictureInPictureParams
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Rational
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.setPadding
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.util.Timer
import kotlin.concurrent.schedule
class MainActivity : FlutterFragmentActivity() {
// قناة الأمان (كما هي)
private val SECURITY_CHANNEL_NAME = "com.siro.siro_rider/security"
private lateinit var securityChannel: MethodChannel
// قناة PiP الجديدة
private val PIP_CHANNEL = "siro/pip"
private var pipEnabled = false // هل الرحلة نشطة ويجب تفعيل PiP عند الخروج؟
// قناة Android Live Update (Android 16 Progress Style)
private val LIVE_UPDATE_CHANNEL = "siro/android_live_update"
private lateinit var liveUpdatePlugin: AndroidLiveUpdatePlugin
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// -------- 1) قناة الأمان --------
securityChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SECURITY_CHANNEL_NAME)
securityChannel.setMethodCallHandler { call, result ->
when (call.method) {
"isNativeRooted" -> {
val isCompromised = isDeviceCompromised()
result.success(isCompromised)
}
else -> {
result.notImplemented()
}
}
}
// -------- 2) قناة PiP (Picture-in-Picture) --------
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"enablePip" -> {
pipEnabled = true
result.success(true)
}
"disablePip" -> {
pipEnabled = false
result.success(true)
}
"enterPip" -> {
val success = enterPipMode()
result.success(success)
}
"isPipSupported" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
}
else -> {
result.notImplemented()
}
}
}
// -------- 3) قناة Android Live Update (Progress Style) --------
liveUpdatePlugin = AndroidLiveUpdatePlugin(this)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, LIVE_UPDATE_CHANNEL)
.setMethodCallHandler { call, result ->
liveUpdatePlugin.handleMethodCall(call, result)
}
}
// -------- PiP Helper Methods --------
private fun enterPipMode(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(9, 16)) // نسبة عمودية مناسبة لعرض الخريطة
.build()
return enterPictureInPictureMode(params)
}
return false
}
// عند ضغط المستخدم على زر الرجوع للشاشة الرئيسية أثناء رحلة نشطة
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (pipEnabled) {
enterPipMode()
}
}
// إعلام Flutter بتغيير وضع PiP
override fun onPictureInPictureModeChanged(
isInPipMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPipMode, newConfig)
// يمكن لاحقاً إرسال حدث لـ Flutter لإخفاء/إظهار عناصر الواجهة
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
MethodChannel(messenger, PIP_CHANNEL)
.invokeMethod("onPipChanged", isInPipMode)
}
}
// ---------------- أمن الجهاز (كما عندك تقريباً) ----------------
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isDeviceCompromised()) {
showSecurityWarningDialog()
}
}
private fun isDeviceCompromised(): Boolean {
return try {
nativeRootCheck() // || !safetyNetCheck()
} catch (e: Exception) {
Log.e("SecurityCheck", "Error during security checks: ${e.message}", e)
true
}
}
private fun nativeRootCheck(): Boolean {
Log.d("SecurityCheck", "Starting native root detection...")
return try {
val isNativeRooted = RootDetection.isNativeRooted()
Log.d("SecurityCheck", "Native root detection result: $isNativeRooted")
isNativeRooted
} catch (e: Exception) {
Log.e("SecurityCheck", "Error in native root detection: ${e.message}", e)
true
}
}
// SafetyNet (معلّق كما هو)
private fun safetyNetCheck(): Boolean {
Log.d("SecurityCheck", "Starting SafetyNet check...")
var isSafe = false
val semaphore = java.util.concurrent.Semaphore(0)
SafetyNetCheck.checkSafetyNet(this, getString(R.string.api_key_safety)) { result ->
isSafe = result
Log.d("SecurityCheck", "SafetyNet check result: $isSafe")
semaphore.release()
}
try {
semaphore.acquire()
} catch (e: InterruptedException) {
Log.e("SecurityCheck", "Interrupted while waiting for SafetyNet check", e)
return false
}
return isSafe
}
private fun showSecurityWarningDialog() {
var secondsRemaining = 10
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal)
progressBar.max = 10
progressBar.progress = 10
val textView = TextView(this)
textView.text = getString(R.string.security_warning_message)
textView.textAlignment = TextView.TEXT_ALIGNMENT_CENTER
val layout = LinearLayout(this)
layout.orientation = LinearLayout.VERTICAL
layout.setPadding(48)
layout.addView(textView)
layout.addView(progressBar)
val dialog =
AlertDialog.Builder(this)
.setTitle(getString(R.string.security_warning_title))
.setView(layout)
.setCancelable(false)
.create()
dialog.show()
val timer = Timer()
timer.schedule(0, 1000) {
secondsRemaining--
runOnUiThread {
progressBar.progress = secondsRemaining
if (secondsRemaining <= 0) {
timer.cancel()
dialog.dismiss()
clearAppDataAndExit()
}
}
}
}
private fun clearAppDataAndExit() {
try {
val packageName = applicationContext.packageName
val runtime = Runtime.getRuntime()
runtime.exec("pm clear $packageName")
} catch (e: Exception) {
clearCache()
clearAppData()
}
finishAffinity()
System.exit(0)
}
private fun clearCache() {
try {
deleteDir(cacheDir)
deleteDir(externalCacheDir)
} catch (e: Exception) {
Log.e("SecurityCheck", "Error clearing cache: ${e.message}", e)
}
}
private fun clearAppData() {
try {
deleteDir(applicationContext.dataDir)
} catch (e: Exception) {
Log.e("SecurityCheck", "Error clearing app data: ${e.message}", e)
}
}
private fun deleteDir(dir: File?): Boolean {
if (dir != null && dir.isDirectory) {
val children = dir.list()
if (children != null) {
for (child in children) {
if (!deleteDir(File(dir, child))) {
return false
}
}
}
}
return dir?.delete() ?: false
}
}
@@ -0,0 +1,9 @@
package com.siro.siro_rider
object RootDetection {
init {
System.loadLibrary("native-lib") // Load the native library
}
external fun isNativeRooted(): Boolean // Declare the external function
}
@@ -0,0 +1,109 @@
import android.content.Context
import android.util.Base64
import android.util.Log
import com.google.android.gms.safetynet.SafetyNet
import com.google.android.gms.safetynet.SafetyNetApi
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import org.json.JSONObject
import java.io.IOException
import java.security.GeneralSecurityException
import java.security.SecureRandom
import java.util.Arrays
object SafetyNetCheck {
private const val TAG = "SafetyNetCheck"
fun checkSafetyNet(context: Context, apiKey: String, callback: (Boolean) -> Unit) {
// Generate a nonce. A good nonce is large, random, and used only once.
val nonce = generateNonce()
SafetyNet.getClient(context).attest(nonce, apiKey)
.addOnSuccessListener { response ->
// Success! Now, *verify* the response.
val jwsResult = response.jwsResult
if (jwsResult != null) {
try {
val isSafe = SafetyNetResponseVerifier.verify(jwsResult)
Log.d(TAG, "SafetyNet verification result: $isSafe")
callback(isSafe) // Now passing a *verified* result.
} catch (e: Exception) {
Log.e(TAG, "Error verifying SafetyNet response: ${e.message}", e)
callback(false) // Treat verification errors as failures.
}
} else {
Log.e(TAG, "SafetyNet jwsResult is null")
callback(false) // Null result is a failure.
}
}
.addOnFailureListener { e ->
Log.e(TAG, "SafetyNet attest API call failed: ${e.message}", e)
callback(false) // API call failure.
}
}
// Helper function to generate a nonce.
private fun generateNonce(): ByteArray {
val byteGenerator = SecureRandom()
val nonce = ByteArray(32)
byteGenerator.nextBytes(nonce)
return nonce
}
}
// Helper class to verify the SafetyNet response.
object SafetyNetResponseVerifier {
private const val TAG = "SafetyNetVerifier"
// This method *must* be implemented on a *backend server* for real security.
// This is just a *simplified example* for demonstration purposes and is
// *not* suitable for production without a backend check.
@Throws(GeneralSecurityException::class, IOException::class)
fun verify(jwsResult: String): Boolean {
// 1. Parse the JWS: Split into header, payload, and signature.
val parts = jwsResult.split(".")
if (parts.size != 3) {
Log.e(TAG, "Invalid JWS format")
return false // Invalid JWS format
}
val header = parts[0]
val payload = parts[1]
val signature = parts[2]
// 2. Decode the payload (it's Base64 encoded).
val decodedPayload = Base64.decode(payload, Base64.DEFAULT)
val payloadJson = JSONObject(String(decodedPayload))
// 3. Check the ctsProfileMatch and basicIntegrity.
val ctsProfileMatch = payloadJson.optBoolean("ctsProfileMatch", false)
val basicIntegrity = payloadJson.optBoolean("basicIntegrity", false)
Log.d(TAG, "ctsProfileMatch: $ctsProfileMatch, basicIntegrity: $basicIntegrity")
// 4. **CRITICAL: In a real application, you *must* send the JWS to your
// backend server for verification. The server should use the
// Google SafetyNet API (or a library that wraps it) to verify
// the signature and check the fields. This prevents attackers
// from tampering with the response on the device.**
//
// // Example (pseudo-code) of what the backend check would do:
// // GoogleCredential credential = ...;
// // SafetyNet safetyNet = new SafetyNet.Builder(httpTransport, jsonFactory)
// // .setApplicationName("YourAppName")
// // .setHttpRequestInitializer(credential)
// // .build();
// // SafetyNetApi.VerifyJwsRequest request = safetyNet.safetynet().verifyJws(jwsResult);
// // SafetyNetApi.VerifyJwsResponse response = request.execute();
// // return response.isValidSignature() && response.getCtsProfileMatch() && response.getBasicIntegrity();
// 5. For this *example* (without a backend), we'll just check the fields.
// This is *NOT SECURE* for production!
return ctsProfileMatch && basicIntegrity
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launcher_icon" />
</item>
</layer-list>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#2C2C2C" />
<corners android:radius="8dp" />
</shape>
</item>
<item android:top="15dp" android:bottom="15dp">
<shape android:shape="line">
<stroke
android:color="#FFFFFF"
android:width="2dp"
android:dashWidth="10dp"
android:dashGap="10dp" />
</shape>
</item>
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
<resources>
<string name="security_warning_title">تحذير أمني</string>
<string name="security_warning_message">تم اكتشاف مشكلة أمنية أو تعديل على هذا الجهاز. لا يمكن
تشغيل التطبيق على هذا الجهاز.</string>
<string name="exit_button">إغلاق التطبيق</string>
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
<string name="label">سيرو</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<!-- <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">-->
<style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<!-- <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">-->
<style name="NormalTheme" parent="Theme.MaterialComponents">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,16 @@
<resources>
<string name="app_name">My App</string>
<!-- <string name="default_notification_channel_id">ride_channel</string> -->
<!-- <string name="default_notification_channel_id">default_channel</string> -->
<string name="default_notification_channel_id">high_importance_channel</string>
<string name="api_key">k</string>
<!-- <string name="api_key">AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64</string> -->
<string name="security_warning_title">Security Warning</string>
<string name="api_key_safety">AIzaSyB04YNW3LbvmQ5lX1t2bOwEU18-KUoovzw</string>
<string name="security_warning_message">A security issue or modification has been detected on
this device. The app cannot run on this device.</string>
<string name="exit_button">Exit App</string>
<string name="device_secure">Device is secure. Proceeding normally.</string>
<string name="label">Siro Rider</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<!-- <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">-->
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<!-- <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">-->
<style name="NormalTheme" parent="Theme.MaterialComponents">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">intaleq.xyz</domain>
<domain includeSubdomains="true">siromove.com</domain>
<pin-set expiration="2028-01-01">
<!-- Primary: ISRG Root X1 (RSA) -->
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHESsl=</pin>
<!-- Backup: ISRG Root X2 (ECDSA) -->
<pin digest="SHA-256">diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</pin>
</pin-set>
</domain-config>
</network-security-config>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>