2026-02-20-overlay

This commit is contained in:
Hamza-Ayed
2026-02-20 17:55:51 +03:00
parent 0b826f6e01
commit d697de9c25
206 changed files with 2635 additions and 1359 deletions

33
trip_overlay_plugin/.gitignore vendored Normal file
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.intaleq_driver.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,465 @@
package com.intaleq_driver.trip_overlay_plugin
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PixelFormat
import android.os.Build
import android.os.CountDownTimer
import android.os.IBinder
import android.util.Log
import android.view.*
import android.widget.*
import androidx.core.app.NotificationCompat
import org.json.JSONObject
/**
* TripOverlayService — Foreground service responsible for:
* 1. Showing a system-level overlay window (SYSTEM_ALERT_WINDOW)
* 2. Displaying trip details inside it
* 3. Handling Accept / Reject actions
* 4. Notifying Flutter via TripOverlayPlugin static callbacks
* 5. Launching the host app to foreground on accept
*/
class TripOverlayService : Service() {
companion object {
const val TAG = "TripOverlayService"
const val ACTION_SHOW = "com.intaleq_driver.SHOW"
const val ACTION_HIDE = "com.intaleq_driver.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 = ""
// ─── Lifecycle ────────────────────────────────────────────────────────────
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
createNotificationChannel()
Log.d(TAG, "Service created")
}
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, 30)
startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose)
}
ACTION_HIDE -> {
dismissOverlay(reason = "programmatic")
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
removeOverlayView()
countDownTimer?.cancel()
isRunning = false
Log.d(TAG, "Service destroyed")
super.onDestroy()
}
// ─── Foreground Notification ──────────────────────────────────────────────
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
"Trip Overlay Service",
NotificationManager.IMPORTANCE_LOW
)
.apply {
description = "Keeps overlay active for incoming trips"
setSound(null, null)
enableVibration(false)
}
(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)
.setSilent(true)
.build()
startForeground(NOTIFICATION_ID, notification)
}
// ─── Overlay Window ───────────────────────────────────────────────────────
private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) {
// Remove any existing overlay first
removeOverlayView()
// Parse trip data
val tripData =
parseTripData(tripDataJson)
?: run {
Log.e(TAG, "Failed to parse trip data")
stopSelf()
return
}
currentTripId = tripData.tripId
// Build overlay view programmatically (no XML required)
val view = buildOverlayView(tripData, autoCloseSeconds)
overlayView = view
// Window layout params
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
// ✅ التعديل هنا: إنزال النافذة بنسبة 18% من طول الشاشة
val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.18).toInt()
}
try {
windowManager?.addView(view, params)
isRunning = true
Log.d(TAG, "Overlay added to window manager")
} catch (e: Exception) {
Log.e(TAG, "Failed to add overlay view: ${e.message}", e)
stopSelf()
return
}
// Start countdown timer
startCountdown(autoCloseSeconds)
}
/**
* Builds the overlay card entirely in code — no XML needed. Override this to customise the UI.
*/
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this
val card =
FrameLayout(ctx).apply {
setBackgroundResource(android.R.drawable.dialog_holo_light_frame)
elevation = 24f
}
val root =
LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(40, 32, 40, 32)
}
card.addView(root)
// ── العنوان والوقت ──
val headerRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val titleText =
TextView(ctx).apply {
text = "🚗 طلب توصيل جديد"
textSize = 18f
setTextColor(Color.parseColor("#1a1a2e"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
headerRow.addView(titleText)
root.addView(headerRow)
root.addView(divider(ctx))
// ── بيانات الرحلة (اسم، انطلاق، مسافة، سعر) ──
// أزلنا الإيميل واكتفينا بالمعلومات المهمة
root.addView(labeledRow(ctx, "👤 الراكب:", trip.passengerName))
root.addView(labeledRow(ctx, "📍 الانطلاق:", trip.pickupAddress))
val statsRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 16, 0, 16)
}
statsRow.addView(statChip(ctx, "${trip.distanceKm} كم", "المسافة"))
statsRow.addView(statChip(ctx, "${trip.estimatedFare} ل.س", "الأجرة"))
root.addView(statsRow)
// ── شريط الوقت ──
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"))
}
root.addView(countdownLabel)
root.addView(progressBar)
// ── أزرار القبول والرفض ──
val buttonsRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
.apply { topMargin = 24 }
}
val rejectBtn =
Button(ctx).apply {
text = "✖ رفض"
textSize = 16f
setTextColor(Color.WHITE)
setBackgroundColor(Color.parseColor("#e74c3c")) // لون أحمر
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 16 }
setOnClickListener { dismissOverlay(reason = "rejected") }
}
val acceptBtn =
Button(ctx).apply {
text = "✔ قبول"
textSize = 16f
setTextColor(Color.WHITE)
setBackgroundColor(Color.parseColor("#27ae60")) // لون أخضر
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setOnClickListener { onTripAccepted() }
}
buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn)
root.addView(buttonsRow)
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card
}
// ─── Countdown Timer ──────────────────────────────────────────────────────
private fun startCountdown(totalSeconds: Int) {
countDownTimer?.cancel()
countDownTimer =
object : CountDownTimer(totalSeconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val secondsLeft = (millisUntilFinished / 1000).toInt()
updateCountdownUI(secondsLeft)
}
override fun onFinish() {
Log.d(TAG, "Countdown finished — auto-dismissing overlay")
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
if (secondsLeft <= 5) {
label.setTextColor(Color.parseColor("#c0392b"))
}
}
// ─── Actions ──────────────────────────────────────────────────────────────
private fun onTripAccepted() {
Log.d(TAG, "Trip accepted: $currentTripId")
countDownTimer?.cancel()
TripOverlayPlugin.notifyTripAccepted(currentTripId)
bringAppToForeground()
removeOverlayView()
stopSelf()
}
private fun dismissOverlay(reason: String) {
Log.d(TAG, "Overlay dismissed — reason: $reason")
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
}
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() {
val packageManager = packageManager
val launchIntent =
packageManager.getLaunchIntentForPackage(packageName)
?: run {
Log.w(TAG, "No launch intent found for $packageName")
return
}
launchIntent.apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP // إضافة مهمة لتنشيط الـ Activity
)
putExtra("acceptedTripId", currentTripId)
}
startActivity(launchIntent)
Log.d(TAG, "Launched app to foreground")
}
// ─── Helpers ──────────────────────────────────────────────────────────────
private fun removeOverlayView() {
overlayView?.let {
try {
windowManager?.removeView(it)
Log.d(TAG, "Overlay view removed")
} catch (e: Exception) {
Log.e(TAG, "Error removing overlay view: ${e.message}")
}
overlayView = null
isRunning = false
}
}
private fun labeledRow(ctx: Context, label: String, value: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(0, 12, 0, 4)
addView(
TextView(ctx).apply {
text = label
textSize = 11f
setTextColor(Color.parseColor("#888888"))
}
)
addView(
TextView(ctx).apply {
text = value
textSize = 14f
setTextColor(Color.parseColor("#1a1a2e"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
}
}
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)
setPadding(12, 16, 12, 16)
setBackgroundColor(Color.parseColor("#f0f4f8"))
addView(
TextView(ctx).apply {
text = value
textSize = 16f
setTextColor(Color.parseColor("#2c3e50"))
gravity = Gravity.CENTER
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
addView(
TextView(ctx).apply {
text = label
textSize = 10f
setTextColor(Color.parseColor("#7f8c8d"))
gravity = Gravity.CENTER
}
)
}
}
private fun divider(ctx: Context): View {
return View(ctx).apply {
setBackgroundColor(Color.parseColor("#e0e0e0"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply {
setMargins(0, 16, 0, 16)
}
}
}
// ─── Data Parsing ─────────────────────────────────────────────────────────
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) {
Log.e(TAG, "JSON parse error: ${e.message}")
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)
}
}

45
trip_overlay_plugin/example/.gitignore vendored Normal file
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,25 @@
// 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();
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final TripOverlayPlugin plugin = TripOverlayPlugin();
final String? version = await plugin.getPlatformVersion();
// The version string depends on the host platform running the test, so
// just assert that some non-empty string is returned.
expect(version?.isNotEmpty, true);
});
}

View File

@@ -0,0 +1,63 @@
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;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _tripOverlayPlugin.getPlatformVersion() ?? '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: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
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: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
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: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
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 'package:trip_overlay_plugin_example/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,29 @@
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>());
});
test('getPlatformVersion', () async {
TripOverlayPlugin tripOverlayPlugin = TripOverlayPlugin();
MockTripOverlayPluginPlatform fakePlatform = MockTripOverlayPluginPlatform();
TripOverlayPluginPlatform.instance = fakePlatform;
expect(await tripOverlayPlugin.getPlatformVersion(), '42');
});
}