first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/

View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "67323de285b00232883f53b84095eb72be97d35c"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: android
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View File

@@ -0,0 +1 @@
TODO: Add your license here.

View File

@@ -0,0 +1,15 @@
# trip_overlay_plugin
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

View File

@@ -0,0 +1,66 @@
group = "com.intaleq_driver.trip_overlay_plugin"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.2.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.intaleq_driver.trip_overlay_plugin"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 24
}
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

View File

@@ -0,0 +1 @@
rootProject.name = 'trip_overlay_plugin'

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.siro.trip_overlay_plugin">
</manifest>

View File

@@ -0,0 +1,179 @@
package com.intaleq_driver.trip_overlay_plugin
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
class TripOverlayPlugin :
FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private var activity: Activity? = null
companion object {
const val CHANNEL = "trip_overlay_plugin"
const val TAG = "TripOverlayPlugin"
const val REQUEST_CODE_OVERLAY = 1001
// Static reference so TripOverlayService can call back into Flutter
var methodChannel: MethodChannel? = null
/**
* Called by TripOverlayService when the driver taps Accept. Sends event Flutter → Dart
* side.
*/
fun notifyTripAccepted(tripId: String) {
methodChannel?.invokeMethod("onTripAccepted", mapOf("tripId" to tripId))
Log.d(TAG, "notifyTripAccepted: $tripId")
}
/** Called by TripOverlayService when the driver taps Reject or timer expires. */
fun notifyTripRejected(tripId: String) {
methodChannel?.invokeMethod("onTripRejected", mapOf("tripId" to tripId))
Log.d(TAG, "notifyTripRejected: $tripId")
}
}
// ─── FlutterPlugin ───────────────────────────────────────────────────────
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
channel = MethodChannel(binding.binaryMessenger, CHANNEL)
channel.setMethodCallHandler(this)
methodChannel = channel
Log.d(TAG, "Plugin attached to engine")
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
methodChannel = null
}
// ─── MethodCallHandler ───────────────────────────────────────────────────
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"isPermissionGranted" -> {
result.success(isOverlayPermissionGranted())
}
"requestPermission" -> {
requestOverlayPermission()
result.success(null)
}
"showOverlay" -> {
val tripDataJson =
call.argument<String>("tripData")
?: run {
result.error("INVALID_ARGS", "tripData is required", null)
return
}
val autoCloseSeconds = call.argument<Int>("autoCloseSeconds") ?: 30
val success = showOverlay(tripDataJson, autoCloseSeconds)
result.success(success)
}
"hideOverlay" -> {
hideOverlay()
result.success(null)
}
"isOverlayActive" -> {
result.success(TripOverlayService.isRunning)
}
else -> result.notImplemented()
}
}
// ─── Permission Helpers ───────────────────────────────────────────────────
private fun isOverlayPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(context)
} else {
true // Pre-M devices don't need runtime permission
}
}
private fun requestOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent =
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context.packageName}")
)
.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
activity?.startActivityForResult(intent, REQUEST_CODE_OVERLAY)
?: context.startActivity(intent)
}
}
// ─── Overlay Control ──────────────────────────────────────────────────────
private fun showOverlay(tripDataJson: String, autoCloseSeconds: Int): Boolean {
if (!isOverlayPermissionGranted()) {
Log.w(TAG, "Overlay permission not granted")
return false
}
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_SHOW
putExtra(TripOverlayService.EXTRA_TRIP_DATA, tripDataJson)
putExtra(TripOverlayService.EXTRA_AUTO_CLOSE_SECONDS, autoCloseSeconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "showOverlay called with tripData: $tripDataJson")
return true
}
private fun hideOverlay() {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_HIDE
}
context.startService(intent)
Log.d(TAG, "hideOverlay called")
}
// ─── ActivityAware ────────────────────────────────────────────────────────
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
// ─── ActivityResultListener ───────────────────────────────────────────────
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQUEST_CODE_OVERLAY) {
Log.d(TAG, "Overlay permission result received")
return true
}
return false
}
}

View File

@@ -0,0 +1,109 @@
package com.intaleq_driver.trip_overlay_plugin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import org.json.JSONObject
/**
* TripOverlayReceiver
*
* A BroadcastReceiver you can trigger directly from your FCM background handler (or anywhere
* outside Flutter context) to show the overlay without needing an active MethodChannel.
*
* Usage from your backgroundMessageHandler in main.dart: — Call the static helper
* [TripOverlayReceiver.show(context, tripDataJson)]
*
* Or register in AndroidManifest and send an explicit broadcast.
*
* Register in AndroidManifest.xml: <receiver
* ```
* android:name="com.trip_overlay.TripOverlayReceiver"
* android:exported="false" />
* ```
*/
class TripOverlayReceiver : BroadcastReceiver() {
companion object {
const val TAG = "TripOverlayReceiver"
const val ACTION = "com.intaleq_driver.SHOW_OVERLAY"
const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_AUTO_CLOSE = "auto_close_seconds"
/**
* Convenience method — call this from your FCM handler (Kotlin/Java) or from a Flutter
* MethodChannel invocation.
*/
fun show(context: Context, tripDataJson: String, autoCloseSeconds: Int = 30) {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_SHOW
putExtra(TripOverlayService.EXTRA_TRIP_DATA, tripDataJson)
putExtra(TripOverlayService.EXTRA_AUTO_CLOSE_SECONDS, autoCloseSeconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "Dispatched show overlay request")
}
/** Hide the overlay programmatically from native side */
fun hide(context: Context) {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_HIDE
}
context.startService(intent)
Log.d(TAG, "Dispatched hide overlay request")
}
/**
* Build a TripData JSON string from FCM message data map. Keys match what your server sends
* inside the FCM data payload.
*
* Expected FCM data keys: tripId, passengerName, pickupAddress, dropoffAddress, distanceKm,
* estimatedFare, estimatedMinutes, pickupLat, pickupLng
*/
fun buildTripJsonFromFcmData(data: Map<String, String>): String {
return try {
JSONObject()
.apply {
put("tripId", data["tripId"] ?: "")
put("passengerName", data["passengerName"] ?: "غير معروف")
put("pickupAddress", data["pickupAddress"] ?: "")
put("dropoffAddress", data["dropoffAddress"] ?: "")
put("distanceKm", data["distanceKm"]?.toDoubleOrNull() ?: 0.0)
put("estimatedFare", data["estimatedFare"]?.toDoubleOrNull() ?: 0.0)
put("estimatedMinutes", data["estimatedMinutes"]?.toIntOrNull() ?: 0)
put("pickupLat", data["pickupLat"]?.toDoubleOrNull() ?: 0.0)
put("pickupLng", data["pickupLng"]?.toDoubleOrNull() ?: 0.0)
put("passengerAvatarUrl", data["passengerAvatarUrl"] ?: "")
}
.toString()
} catch (e: Exception) {
Log.e(TAG, "Error building trip JSON: ${e.message}")
"{}"
}
}
}
// ─── Broadcast received ───────────────────────────────────────────────────
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION) return
val tripData =
intent.getStringExtra(EXTRA_TRIP_DATA)
?: run {
Log.e(TAG, "No trip data in broadcast intent")
return
}
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE, 30)
show(context, tripData, autoClose)
Log.d(TAG, "Received broadcast, showing overlay")
}
}

View File

@@ -0,0 +1,560 @@
package com.intaleq_driver.trip_overlay_plugin
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.drawable.GradientDrawable
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.os.Build
import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.*
import android.widget.*
import androidx.core.app.NotificationCompat
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
import org.json.JSONObject
class TripOverlayService : Service() {
companion object {
const val TAG = "TripOverlayService"
const val ACTION_SHOW = "SHOW"
const val ACTION_HIDE = "HIDE"
const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_AUTO_CLOSE_SECONDS = "auto_close_seconds"
private const val NOTIFICATION_ID = 9900
private const val CHANNEL_ID = "trip_overlay_service_channel"
@Volatile
var isRunning: Boolean = false
private set
}
private var windowManager: WindowManager? = null
private var overlayView: View? = null
private var countDownTimer: CountDownTimer? = null
private var currentTripId: String = ""
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_SHOW -> {
val tripDataJson = intent.getStringExtra(EXTRA_TRIP_DATA) ?: return START_NOT_STICKY
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE_SECONDS, 15)
startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose)
}
ACTION_HIDE -> dismissOverlay(reason = "programmatic")
}
return START_NOT_STICKY
}
override fun onDestroy() {
removeOverlayView()
countDownTimer?.cancel()
stopSound() // 🔴 إيقاف الصوت عند التدمير
isRunning = false
super.onDestroy()
}
// ==========================================================
// 🔴 الصوت والإشعارات 🔴
// ==========================================================
private fun playSound() {
try {
// استخدام صوت التنبيه الافتراضي القوي في الجهاز
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
mediaPlayer = MediaPlayer.create(this, soundUri)
mediaPlayer?.isLooping = true // تكرار الصوت حتى يتخذ السائق قراراً
mediaPlayer?.start()
} catch (e: Exception) {
Log.e(TAG, "Error playing sound: ${e.message}")
}
}
private fun stopSound() {
try {
mediaPlayer?.takeIf { it.isPlaying }?.stop()
mediaPlayer?.release()
mediaPlayer = null
} catch (e: Exception) {
Log.e(TAG, "Error stopping sound: ${e.message}")
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
"Trip Overlay Service",
NotificationManager.IMPORTANCE_LOW
)
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel)
}
}
private fun startForegroundWithNotification() {
val notification =
NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("طلب رحلة جديد")
.setContentText("يوجد طلب رحلة في انتظارك")
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
startForeground(NOTIFICATION_ID, notification)
}
// ==========================================================
// 🔴 بناء وتصميم النافذة (Premium UI) 🔴
// ==========================================================
private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) {
removeOverlayView()
val tripData =
parseTripData(tripDataJson)
?: run {
stopSelf()
return
}
currentTripId = tripData.tripId
val view = buildOverlayView(tripData, autoCloseSeconds)
overlayView = view
val type =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else @Suppress("DEPRECATION") WindowManager.LayoutParams.TYPE_PHONE
val params =
WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT,
type,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
PixelFormat.TRANSLUCENT
)
.apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.15).toInt() // النزول للأسفل بنسبة 15%
}
try {
windowManager?.addView(view, params)
isRunning = true
playSound() // 🔴 تشغيل الصوت
startCountdown(autoCloseSeconds)
} catch (e: Exception) {
stopSelf()
}
}
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this
// الخلفية الرئيسية بستايل الكروت الحديثة (حواف دائرية كبيرة)
val card =
FrameLayout(ctx).apply {
background =
GradientDrawable().apply {
cornerRadius = 60f
setColor(Color.WHITE)
setStroke(2, Color.parseColor("#E0E0E0")) // إطار خفيف
}
elevation = 40f
setPadding(0, 0, 0, 0)
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
.apply {
setMargins(40, 0, 40, 0) // هوامش جانبية
}
}
val root = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
card.addView(root)
// ── 1. صورة الخريطة (Static Map) ──
val mapImageView =
ImageView(ctx).apply {
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
350
) // ارتفاع الخريطة
scaleType = ImageView.ScaleType.CENTER_CROP
setBackgroundColor(Color.parseColor("#F3F4F6")) // لون رمادي فاتح كخلفية بديلة
}
root.addView(mapImageView)
// 🔴 دالة تحميل الخريطة (شغالة وتحتاج فقط مفتاحك)
loadStaticMap(mapImageView, trip.pickupLat, trip.pickupLng)
// ── حاوية التفاصيل ──
val detailsContainer =
LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(40, 30, 40, 40)
}
root.addView(detailsContainer)
// ── 2. السعر والمسافة (خط كبير وواضح) ──
val priceRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
val priceText =
TextView(ctx).apply {
text = "${trip.estimatedFare} ل.س"
textSize = 24f
setTextColor(Color.parseColor("#27AE60")) // لون أخضر فخم
typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
val distanceText =
TextView(ctx).apply {
text = "المسافة: ${trip.distanceKm} كم"
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
priceRow.addView(priceText)
priceRow.addView(distanceText)
detailsContainer.addView(priceRow)
detailsContainer.addView(divider(ctx, 30))
// ── 3. المواقع (الانطلاق والوصول) ──
detailsContainer.addView(labeledRow(ctx, "🟢 من:", trip.pickupAddress))
detailsContainer.addView(labeledRow(ctx, "🔴 إلى:", trip.dropoffAddress))
detailsContainer.addView(labeledRow(ctx, "👤 الراكب:", trip.passengerName))
detailsContainer.addView(divider(ctx, 30))
// ── 4. شريط الوقت ──
val countdownLabel =
TextView(ctx).apply {
text = "ينتهي خلال $autoCloseSeconds ثانية"
textSize = 12f
setTextColor(Color.parseColor("#E74C3C"))
gravity = Gravity.CENTER
id = android.R.id.text1
}
val progressBar =
ProgressBar(ctx, null, android.R.attr.progressBarStyleHorizontal).apply {
max = autoCloseSeconds
progress = autoCloseSeconds
id = android.R.id.progress
progressTintList =
android.content.res.ColorStateList.valueOf(Color.parseColor("#E74C3C"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 15)
.apply {
topMargin = 10
bottomMargin = 30
}
}
detailsContainer.addView(countdownLabel)
detailsContainer.addView(progressBar)
// ── 5. الأزرار (تصميم كبسولة فخم) ──
val buttonsRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val rejectBtn =
Button(ctx).apply {
text = "رفض"
textSize = 16f
setTextColor(Color.parseColor("#E74C3C")) // نص أحمر
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#FDEDEC")) // خلفية حمراء باهتة فخمة
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 20 }
setOnClickListener { dismissOverlay(reason = "rejected") }
}
val acceptBtn =
Button(ctx).apply {
text = "قبول الطلب"
textSize = 16f
setTextColor(Color.WHITE)
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#27AE60")) // لون أخضر فخم
}
layoutParams =
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.5f
) // زر القبول أكبر قليلاً
setOnClickListener { onTripAccepted() }
}
buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn)
detailsContainer.addView(buttonsRow)
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card
}
// 🔴 دالة احترافية لقراءة المفتاح المحقون من Gradle 🔴
private fun getGoogleMapsApiKey(): String {
return try {
// الوصول إلى بيانات التطبيق في الـ Manifest
val appInfo =
packageManager.getApplicationInfo(
packageName,
android.content.pm.PackageManager.GET_META_DATA
)
val bundle = appInfo.metaData
// قراءة المفتاح (تأكد أن هذا هو نفس الاسم الموجود في AndroidManifest.xml لديك)
val apiKey = bundle?.getString("com.google.android.geo.API_KEY")
apiKey ?: "" // إرجاع المفتاح أو نص فارغ إذا لم يجده
} catch (e: Exception) {
Log.e(TAG, "❌ فشل في قراءة مفتاح الخريطة من Manifest: ${e.message}")
""
}
}
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
private fun loadStaticMap(imageView: ImageView, lat: Double, lng: Double) {
if (lat == 0.0 || lng == 0.0) {
Log.e(TAG, "⚠️ الإحداثيات صفر، تم تجاهل تحميل الخريطة")
return
}
// 🟢 جلب المفتاح بأمان تام من الـ Properties عبر الـ Manifest 🟢
val apiKey = getGoogleMapsApiKey()
if (apiKey.isEmpty()) {
Log.e(TAG, "⚠️ مفتاح جوجل ماب غير موجود أو فارغ!")
return
}
// رابط احترافي: زووم مناسب، وضع خريطة نظيف، ودبوس أخضر بارز
val mapUrl =
"https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x350&maptype=roadmap&markers=color:0x27AE60%7C$lat,$lng&key=$apiKey"
thread {
try {
val url = URL(mapUrl)
val connection = url.openConnection() as HttpURLConnection
connection.doInput = true
connection.connectTimeout = 5000 // تحديد وقت أقصى للاتصال
connection.readTimeout = 5000
connection.connect()
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val bitmap = BitmapFactory.decodeStream(connection.inputStream)
// عرض الصورة في الواجهة (Main Thread)
Handler(Looper.getMainLooper()).post { imageView.setImageBitmap(bitmap) }
} else {
Log.e(TAG, "❌ خطأ من سيرفر جوجل: ${connection.responseCode}")
}
} catch (e: Exception) {
Log.e(TAG, "❌ فشل الاتصال بخدمة الخرائط: ${e.message}")
}
}
}
// ==========================================================
// 🔴 أدوات مساعدة للتصميم 🔴
// ==========================================================
private fun labeledRow(ctx: Context, label: String, value: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 8, 0, 8)
gravity = Gravity.CENTER_VERTICAL
addView(
TextView(ctx).apply {
text = label
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
.apply { rightMargin = 16 }
}
)
addView(
TextView(ctx).apply {
text = value
textSize = 15f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
}
)
}
}
private fun statChip(ctx: Context, value: String, label: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
addView(
TextView(ctx).apply {
text = value
textSize = 16f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
addView(
TextView(ctx).apply {
text = label
textSize = 12f
setTextColor(Color.parseColor("#95A5A6"))
}
)
}
}
private fun divider(ctx: Context, margin: Int = 16): View {
return View(ctx).apply {
setBackgroundColor(Color.parseColor("#EEEEEE"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 2).apply {
setMargins(0, margin, 0, margin)
}
}
}
// ==========================================================
// 🔴 التحكم والمؤقت 🔴
// ==========================================================
private fun startCountdown(totalSeconds: Int) {
countDownTimer?.cancel()
countDownTimer =
object : CountDownTimer(totalSeconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
updateCountdownUI((millisUntilFinished / 1000).toInt())
}
override fun onFinish() {
dismissOverlay(reason = "timeout")
}
}
.start()
}
private fun updateCountdownUI(secondsLeft: Int) {
val tagMap = overlayView?.tag as? Map<*, *> ?: return
val label = tagMap["countdownLabel"] as? TextView ?: return
val bar = tagMap["progressBar"] as? ProgressBar ?: return
label.text = "ينتهي خلال $secondsLeft ثانية"
bar.progress = secondsLeft
}
private fun onTripAccepted() {
stopSound() // 🔴
TripOverlayPlugin.notifyTripAccepted(currentTripId)
bringAppToForeground()
removeOverlayView()
stopSelf()
}
private fun dismissOverlay(reason: String) {
stopSound() // 🔴
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
}
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return
launchIntent.apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
Intent.FLAG_ACTIVITY_SINGLE_TOP
)
putExtra("acceptedTripId", currentTripId)
}
startActivity(launchIntent)
}
private fun removeOverlayView() {
overlayView?.let { windowManager?.removeView(it) }
overlayView = null
isRunning = false
}
private fun parseTripData(json: String): TripInfo? {
return try {
val obj = JSONObject(json)
TripInfo(
tripId = obj.getString("tripId"),
passengerName = obj.getString("passengerName"),
pickupAddress = obj.getString("pickupAddress"),
dropoffAddress = obj.getString("dropoffAddress"),
distanceKm = obj.getDouble("distanceKm"),
estimatedFare = obj.getDouble("estimatedFare"),
estimatedMinutes = obj.getInt("estimatedMinutes"),
pickupLat = obj.getDouble("pickupLat"),
pickupLng = obj.getDouble("pickupLng")
)
} catch (e: Exception) {
null
}
}
data class TripInfo(
val tripId: String,
val passengerName: String,
val pickupAddress: String,
val dropoffAddress: String,
val distanceKm: Double,
val estimatedFare: Double,
val estimatedMinutes: Int,
val pickupLat: Double,
val pickupLng: Double
)
}

View File

@@ -0,0 +1,27 @@
package com.intaleq_driver.trip_overlay_plugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.mockito.Mockito
import kotlin.test.Test
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class TripOverlayPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = TripOverlayPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}

View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,16 @@
# trip_overlay_plugin_example
Demonstrates how to use the trip_overlay_plugin plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.intaleq_driver.trip_overlay_plugin_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.intaleq_driver.trip_overlay_plugin_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

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

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="trip_overlay_plugin_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<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>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.intaleq_driver.trip_overlay_plugin_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -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:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -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/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?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">
<!-- 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">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?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">
<!-- 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">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

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

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,16 @@
// This is a basic Flutter integration test.
//
// Since integration tests run in a full Flutter application, they can interact
// with the host side of a plugin implementation, unlike Dart unit tests.
//
// For more information about Flutter integration tests, please see
// https://flutter.dev/to/integration-testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _tripOverlayPlugin = TripOverlayPlugin();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion = 'Unknown platform version';
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
// TODO: Replace with the correct method from TripOverlayPlugin
platformVersion = 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Plugin example app')),
body: Center(child: Text('Running on: $_platformVersion\n')),
),
);
}
}

View File

@@ -0,0 +1,283 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.dev"
source: hosted
version: "5.0.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.9"
trip_overlay_plugin:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
sdks:
dart: ">=3.10.8 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@@ -0,0 +1,85 @@
name: trip_overlay_plugin_example
description: "Demonstrates how to use the trip_overlay_plugin plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ^3.10.8
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
trip_overlay_plugin:
# When depending on this package from a real application you should use:
# trip_overlay_plugin: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) =>
widget is Text && widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}

View File

@@ -0,0 +1,168 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// Model for trip data passed to the overlay
class TripData {
final String tripId;
final String passengerName;
final String pickupAddress;
final String dropoffAddress;
final double distanceKm;
final double estimatedFare;
final int estimatedMinutes;
final double pickupLat;
final double pickupLng;
final String? passengerAvatarUrl;
TripData({
required this.tripId,
required this.passengerName,
required this.pickupAddress,
required this.dropoffAddress,
required this.distanceKm,
required this.estimatedFare,
required this.estimatedMinutes,
required this.pickupLat,
required this.pickupLng,
this.passengerAvatarUrl,
});
Map<String, dynamic> toMap() => {
'tripId': tripId,
'passengerName': passengerName,
'pickupAddress': pickupAddress,
'dropoffAddress': dropoffAddress,
'distanceKm': distanceKm,
'estimatedFare': estimatedFare,
'estimatedMinutes': estimatedMinutes,
'pickupLat': pickupLat,
'pickupLng': pickupLng,
'passengerAvatarUrl': passengerAvatarUrl ?? '',
};
factory TripData.fromMap(Map<String, dynamic> map) => TripData(
tripId: map['tripId'] ?? '',
passengerName: map['passengerName'] ?? '',
pickupAddress: map['pickupAddress'] ?? '',
dropoffAddress: map['dropoffAddress'] ?? '',
distanceKm: (map['distanceKm'] ?? 0.0).toDouble(),
estimatedFare: (map['estimatedFare'] ?? 0.0).toDouble(),
estimatedMinutes: map['estimatedMinutes'] ?? 0,
pickupLat: (map['pickupLat'] ?? 0.0).toDouble(),
pickupLng: (map['pickupLng'] ?? 0.0).toDouble(),
passengerAvatarUrl: map['passengerAvatarUrl'],
);
factory TripData.fromJson(String json) =>
TripData.fromMap(jsonDecode(json) as Map<String, dynamic>);
String toJson() => jsonEncode(toMap());
}
/// Result returned when the driver accepts a trip
class TripAcceptedResult {
final String tripId;
final DateTime acceptedAt;
TripAcceptedResult({required this.tripId, required this.acceptedAt});
}
/// Main plugin class — single entry point for Flutter side
class TripOverlayPlugin {
static const MethodChannel _channel = MethodChannel('trip_overlay_plugin');
// Stream controller for trip accepted events coming FROM Android overlay
static final StreamController<TripAcceptedResult> _tripAcceptedController =
StreamController<TripAcceptedResult>.broadcast();
// Stream controller for trip rejected/expired events
static final StreamController<String> _tripRejectedController =
StreamController<String>.broadcast();
static bool _isInitialized = false;
/// Stream that fires when the driver taps "Accept" in the overlay
static Stream<TripAcceptedResult> get onTripAccepted =>
_tripAcceptedController.stream;
/// Stream that fires when the driver rejects or overlay times out
static Stream<String> get onTripRejected => _tripRejectedController.stream;
/// Initialize the plugin — call this once in main() or initState()
static Future<void> initialize() async {
if (_isInitialized) return;
_channel.setMethodCallHandler(_handleMethodCall);
_isInitialized = true;
}
/// Handle incoming calls FROM Android → Flutter
static Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onTripAccepted':
final tripId = call.arguments['tripId'] as String;
_tripAcceptedController.add(
TripAcceptedResult(tripId: tripId, acceptedAt: DateTime.now()),
);
break;
case 'onTripRejected':
final tripId = call.arguments['tripId'] as String;
_tripRejectedController.add(tripId);
break;
default:
throw PlatformException(
code: 'UNKNOWN_METHOD',
message: 'Method ${call.method} not implemented',
);
}
}
/// Check if SYSTEM_ALERT_WINDOW permission is granted
static Future<bool> isPermissionGranted() async {
final result = await _channel.invokeMethod<bool>('isPermissionGranted');
return result ?? false;
}
/// Open system settings to grant SYSTEM_ALERT_WINDOW permission
static Future<void> requestPermission() async {
await _channel.invokeMethod('requestPermission');
}
/// Show the trip overlay with the given [tripData]
/// [autoCloseSeconds] — how long before auto-dismiss (default 30s)
static Future<bool> showOverlay(
TripData tripData, {
int autoCloseSeconds = 30,
}) async {
final granted = await isPermissionGranted();
if (!granted) {
await requestPermission();
return false;
}
final result = await _channel.invokeMethod<bool>('showOverlay', {
'tripData': tripData.toJson(),
'autoCloseSeconds': autoCloseSeconds,
});
return result ?? false;
}
/// Programmatically close the overlay (e.g. if trip was cancelled)
static Future<void> hideOverlay() async {
await _channel.invokeMethod('hideOverlay');
}
/// Check if the overlay is currently visible
static Future<bool> isOverlayActive() async {
final result = await _channel.invokeMethod<bool>('isOverlayActive');
return result ?? false;
}
/// Dispose streams — call in app's dispose()
static void dispose() {
_tripAcceptedController.close();
_tripRejectedController.close();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'trip_overlay_plugin_platform_interface.dart';
/// An implementation of [TripOverlayPluginPlatform] that uses method channels.
class MethodChannelTripOverlayPlugin extends TripOverlayPluginPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('trip_overlay_plugin');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'trip_overlay_plugin_method_channel.dart';
abstract class TripOverlayPluginPlatform extends PlatformInterface {
/// Constructs a TripOverlayPluginPlatform.
TripOverlayPluginPlatform() : super(token: _token);
static final Object _token = Object();
static TripOverlayPluginPlatform _instance = MethodChannelTripOverlayPlugin();
/// The default instance of [TripOverlayPluginPlatform] to use.
///
/// Defaults to [MethodChannelTripOverlayPlugin].
static TripOverlayPluginPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [TripOverlayPluginPlatform] when
/// they register themselves.
static set instance(TripOverlayPluginPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

View File

@@ -0,0 +1,70 @@
name: trip_overlay_plugin
description: "A new Flutter plugin project."
version: 0.0.1
homepage:
environment:
sdk: ^3.10.8
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.intaleq_driver.trip_overlay_plugin
pluginClass: TripOverlayPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,27 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelTripOverlayPlugin platform = MethodChannelTripOverlayPlugin();
const MethodChannel channel = MethodChannel('trip_overlay_plugin');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin_platform_interface.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockTripOverlayPluginPlatform
with MockPlatformInterfaceMixin
implements TripOverlayPluginPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
void main() {
final TripOverlayPluginPlatform initialPlatform = TripOverlayPluginPlatform.instance;
test('$MethodChannelTripOverlayPlugin is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelTripOverlayPlugin>());
});
}