location background activity

This commit is contained in:
mohamadmahdi jebeli 2025-08-03 15:44:57 +03:30
parent 5d00779bbf
commit f4cd446cde
17 changed files with 590 additions and 252 deletions

View File

@ -14,29 +14,26 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = "1.8"
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.proxibuy"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled = true
}
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")
}
}
@ -45,3 +42,8 @@ android {
flutter {
source = "../.."
}
dependencies {
// این نسخه به آخرین نسخه مورد نیاز آپدیت شد
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

View File

@ -1,18 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:label="Proxibuy"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- android:networkSecurityConfig="@xml/network_security_config"> -->
<activity
android:name=".MainActivity"
android:exported="true"
@ -22,10 +23,7 @@
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"
@ -35,17 +33,16 @@
<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 -->
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="location" />
<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"/>

View File

@ -7,4 +7,5 @@ class ApiConfig {
static const String getFavoriteCategories = "/user/getfavoriteCategory";
static const String addReservation = "/reservation/add";
static const String getReservations = "/reservation/get";
static const String updateFcmToken = "/user/firebaseUpdate";
}

View File

@ -149,7 +149,7 @@ class OfferModel extends Equatable {
rating: 0.0,
ratingCount: 0,
comments: [],
discountInfo: json['Description'],
discountInfo: json['Description'] ?? 'توضیحات موجود نیست',
);
}
@ -157,7 +157,29 @@ class OfferModel extends Equatable {
imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
@override
List<Object?> get props => [id];
List<Object?> get props => [
id,
storeName,
title,
discount,
imageUrls,
category,
distanceInMeters,
expiryTime,
address,
workingHours,
discountType,
isOpen,
rating,
ratingCount,
latitude,
longitude,
originalPrice,
finalPrice,
features,
discountInfo,
comments,
];
String get distanceAsString {
if (distanceInMeters < 1000) {

View File

@ -1,7 +1,6 @@
// lib/main.dart
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -13,15 +12,35 @@ import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/services/background_service.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'core/config/app_colors.dart';
import 'package:proxibuy/presentation/pages/splash_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HttpOverrides.global = MyHttpOverrides();
Future<String?> getFcmToken() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
String? token = await messaging.getToken();
print("🔥 Firebase Messaging Token: $token");
return token;
}
// FirebaseMessaging.instance.onTokenRefresh
// .listen((fcmToken) {
// // TODO: If necessary send token to application server.
// // Note: This callback is fired at each app startup and whenever a new
// // token is generated.
// })
// .onError((err) {
// // Error getting token.
// });
WidgetsFlutterBinding.ensureInitialized();
await initializeService();
HttpOverrides.global = MyHttpOverrides();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Animate.restartOnHotReload = true;
runApp(const MyApp());
@ -34,22 +53,15 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
RepositoryProvider<MqttService>(
create: (context) => MqttService(),
),
RepositoryProvider<MqttService>(create: (context) => MqttService()),
BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
),
BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(),
),
BlocProvider<OffersBloc>(
create: (context) => OffersBloc(),
),
BlocProvider<ReservationCubit>(create: (context) => ReservationCubit()),
BlocProvider<OffersBloc>(create: (context) => OffersBloc()),
BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc(),
),
],
child: MaterialApp(
title: 'Proxibuy',

View File

@ -1,5 +1,3 @@
// lib/presentation/auth/bloc/auth_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
@ -31,10 +29,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<VerifyOTPEvent>(_onVerifyOTP);
on<UpdateUserInfoEvent>(_onUpdateUserInfo);
on<LogoutEvent>(_onLogout);
on<SendFcmTokenEvent>(_onSendFcmToken);
}
Future<void> _onCheckAuthStatus(
CheckAuthStatusEvent event, Emitter<AuthState> emit) async {
CheckAuthStatusEvent event,
Emitter<AuthState> emit,
) async {
final token = await _storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) {
emit(AuthSuccess());
@ -52,10 +53,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
);
if (isClosed) return;
if (response.statusCode == 200) {
emit(AuthCodeSentSuccess(
phone: event.phoneNumber,
countryCode: event.countryCode,
));
emit(
AuthCodeSentSuccess(
phone: event.phoneNumber,
countryCode: event.countryCode,
),
);
} else {
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
}
@ -65,7 +68,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
}
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async {
Future<void> _onVerifyOTP(
VerifyOTPEvent event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final response = await _dio.post(
@ -97,8 +103,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
Future<void> _onUpdateUserInfo(
UpdateUserInfoEvent event, Emitter<AuthState> emit) async {
debugPrint("AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}");
UpdateUserInfoEvent event,
Emitter<AuthState> emit,
) async {
debugPrint(
"AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}",
);
emit(AuthLoading());
try {
final token = await _storage.read(key: 'accessToken');
@ -115,30 +125,59 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
debugPrint("AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}");
debugPrint(
"AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}",
);
if (isClosed) {
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
return;
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
return;
}
if (response.statusCode == 200) {
debugPrint("AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...");
debugPrint(
"AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...",
);
emit(AuthSuccess());
} else {
debugPrint("AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}");
debugPrint(
"AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}",
);
emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات'));
}
} on DioException catch (e) {
debugPrint("AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}");
debugPrint(
"AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}",
);
if (isClosed) return;
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
}
}
Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async {
await _storage.deleteAll();
emit(AuthInitial());
}
Future<void> _onSendFcmToken(
SendFcmTokenEvent event,
Emitter<AuthState> emit,
) async {
try {
final token = await _storage.read(key: 'accessToken');
if (token == null) {
emit(const AuthFailure("شما وارد نشده‌اید."));
return;
}
await _dio.post(
ApiConfig.baseUrl + ApiConfig.updateFcmToken,
data: {'Token': event.fcmToken},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
print("Firebase token: ${event.fcmToken}");
} on DioException catch (e) {
debugPrint("Error sending FCM token: ${e.response?.data['message']}");
}
}
}

View File

@ -26,3 +26,8 @@ class UpdateUserInfoEvent extends AuthEvent {
UpdateUserInfoEvent({required this.name, required this.gender});
}
class SendFcmTokenEvent extends AuthEvent {
final String fcmToken;
SendFcmTokenEvent({required this.fcmToken});
}

View File

@ -1,3 +1,5 @@
// lib/presentation/pages/offers_page.dart
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
@ -5,6 +7,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.dart';
@ -23,7 +26,6 @@ import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/gps_dialog.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
class OffersPage extends StatefulWidget {
@ -38,10 +40,7 @@ class OffersPage extends StatefulWidget {
class _OffersPageState extends State<OffersPage> {
List<String> _selectedCategories = [];
StreamSubscription? _locationServiceSubscription;
StreamSubscription? _mqttMessageSubscription;
StreamSubscription? _connectivitySubscription;
Timer? _locationTimer;
bool _isSubscribedToOffers = false;
bool _isGpsEnabled = false;
bool _isConnectedToInternet = true;
@ -57,8 +56,6 @@ class _OffersPageState extends State<OffersPage> {
@override
void dispose() {
_locationServiceSubscription?.cancel();
_mqttMessageSubscription?.cancel();
_locationTimer?.cancel();
_connectivitySubscription?.cancel();
super.dispose();
}
@ -67,7 +64,8 @@ class _OffersPageState extends State<OffersPage> {
final connectivityResult = await Connectivity().checkConnectivity();
if (!mounted) return;
setState(() {
_isConnectedToInternet = !connectivityResult.contains(ConnectivityResult.none);
_isConnectedToInternet =
!connectivityResult.contains(ConnectivityResult.none);
});
}
@ -80,10 +78,37 @@ class _OffersPageState extends State<OffersPage> {
}
});
}
await _loadPreferences();
_initLocationListener();
_subscribeToUserOffersOnLoad();
_listenToBackgroundService();
}
void _listenToBackgroundService() {
FlutterBackgroundService().on('update').listen((event) {
if (event == null || event['offers'] == null) return;
final data = event['offers']['data'];
if (data == null || data is! List) {
if (mounted) {
context.read<OffersBloc>().add(const OffersReceivedFromMqtt([]));
}
return;
}
try {
List<OfferModel> offers = data
.whereType<Map<String, dynamic>>()
.map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
}
} catch (e, stackTrace) {
debugPrint("❌ Error parsing offers from Background Service: $e");
debugPrint(stackTrace.toString());
}
});
}
void _initConnectivityListener() {
@ -143,14 +168,6 @@ class _OffersPageState extends State<OffersPage> {
}
}
Future<void> _subscribeToUserOffersOnLoad() async {
final storage = const FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID != null && mounted) {
_subscribeToUserOffers(userID);
}
}
void _initLocationListener() {
_checkInitialGpsStatus();
_locationServiceSubscription =
@ -160,11 +177,7 @@ class _OffersPageState extends State<OffersPage> {
setState(() {
_isGpsEnabled = isEnabled;
});
if (isEnabled) {
_startSendingLocationUpdates();
} else {
debugPrint("❌ Location Service Disabled. Stopping updates.");
_locationTimer?.cancel();
if (!isEnabled) {
context.read<OffersBloc>().add(ClearOffers());
}
}
@ -177,105 +190,9 @@ class _OffersPageState extends State<OffersPage> {
setState(() {
_isGpsEnabled = status;
});
if (_isGpsEnabled) {
_startSendingLocationUpdates();
}
}
}
void _startSendingLocationUpdates() {
debugPrint("🚀 Starting periodic location updates.");
_locationTimer?.cancel();
_locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
_sendLocationUpdate();
});
_sendLocationUpdate();
}
Future<void> _sendLocationUpdate() async {
if (!_isConnectedToInternet || !_isGpsEnabled) return;
final mqttService = context.read<MqttService>();
if (!mqttService.isConnected) {
debugPrint("⚠️ MQTT not connected in OffersPage. Cannot send location.");
return;
}
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
debugPrint("🚫 Location permission denied by user.");
return;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID == null) {
debugPrint("⚠️ UserID not found. Cannot send location.");
return;
}
final payload = {
"userID": userID,
"lat":32.6685,
"lng": 51.6826
};
mqttService.publish("proxybuy/sendGps", payload);
} catch (e) {
debugPrint("❌ Error sending location update in OffersPage: $e");
}
}
void _subscribeToUserOffers(String userID) {
if (_isSubscribedToOffers) return;
final mqttService = context.read<MqttService>();
if (!mqttService.isConnected) {
debugPrint("⚠️ Cannot subscribe. MQTT client is not connected.");
return;
}
final topic = 'user-proxybuy/$userID';
mqttService.subscribe(topic);
_isSubscribedToOffers = true;
_mqttMessageSubscription = mqttService.messages.listen((message) {
final data = message['data'];
if (data == null || data is! List) {
if (mounted) {
context.read<OffersBloc>().add(const OffersReceivedFromMqtt([]));
}
return;
}
try {
List<OfferModel> offers = data
.whereType<Map<String, dynamic>>()
.map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
}
} catch (e, stackTrace) {
debugPrint("❌ Error parsing offers from MQTT: $e");
debugPrint(stackTrace.toString());
}
});
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
final savedCategories =

View File

@ -3,12 +3,14 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/pages/onboarding_page.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@ -28,9 +30,24 @@ class _SplashScreenState extends State<SplashScreen> {
Future.delayed(const Duration(seconds: 2), _startProcess);
}
Future<String?> getFcmToken() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
try {
String? token = await messaging.getToken();
debugPrint("🔥 Firebase Messaging Token: $token");
return token;
} catch(e) {
debugPrint("Error getting FCM token: $e");
return null;
}
}
Future<void> _startProcess() async {
if (!mounted) return;
await _requestPermissions();
final hasInternet = await _checkInternet();
if (!hasInternet) {
setState(() {
@ -60,11 +77,18 @@ class _SplashScreenState extends State<SplashScreen> {
return;
}
final String? fcmToken = await getFcmToken();
final mqttService = context.read<MqttService>();
final storage = const FlutterSecureStorage();
final token = await storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) {
if (fcmToken != null) {
context.read<AuthBloc>().add(SendFcmTokenEvent(fcmToken: fcmToken));
}
if (mqttService.isConnected) {
_navigateToOffers();
return;
@ -186,4 +210,13 @@ class _SplashScreenState extends State<SplashScreen> {
),
);
}
Future<void> _requestPermissions() async {
await Permission.notification.request();
var status = await Permission.location.request();
if (status.isGranted) {
await Permission.locationAlways.request();
}
}
}

View File

@ -339,8 +339,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 9,
horizontal: 15,
vertical: 12,
),
decoration: BoxDecoration(
color: AppColors.singleOfferType,
@ -351,7 +351,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 17,
fontSize: 13,
overflow: TextOverflow.ellipsis
),
),
),
@ -365,7 +366,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Text(
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
style: const TextStyle(
fontSize: 14,
fontSize: 12,
color: AppColors.singleOfferType,
fontWeight: FontWeight.normal,
),
@ -374,7 +375,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Text(
formatCurrency.format(widget.offer.originalPrice),
style: TextStyle(
fontSize: 14,
fontSize: 13,
color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough,
),
@ -386,7 +387,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
'${formatCurrency.format(widget.offer.finalPrice)} تومان',
style: const TextStyle(
color: AppColors.singleOfferType,
fontSize: 18,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),

View File

@ -0,0 +1,107 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/services/mqtt_service.dart';
const notificationChannelId = 'proxibuy_foreground_service';
const notificationId = 888;
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final MqttService mqttService = MqttService();
const storage = FlutterSecureStorage();
if (service is AndroidServiceInstance) {
service.setForegroundNotificationInfo(
title: "ProxiBuy فعال است",
content: "در حال جستجو برای تخفیف های اطراف شما...",
);
service.on('stopService').listen((event) {
service.stopSelf();
});
}
mqttService.messages.listen((data) {
service.invoke('update', {'offers': data});
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service: Sending location...");
var locationStatus = await Permission.location.status;
if (!locationStatus.isGranted) {
debugPrint("Background Service: Location permission not granted.");
return;
}
try {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final token = await storage.read(key: 'accessToken');
final userID = await storage.read(key: 'userID');
if (token != null && userID != null) {
if (!mqttService.isConnected) {
await mqttService.connect(token);
final topic = 'user-proxybuy/$userID';
mqttService.subscribe(topic);
}
if (mqttService.isConnected) {
final payload = {
"userID": userID,
"lat": position.latitude,
"lng": position.longitude
};
mqttService.publish("proxybuy/sendGps", payload);
debugPrint("Background Service: GPS sent successfully.");
}
}
} catch (e) {
debugPrint("❌ Background Service Error: $e");
}
});
}
Future<void> initializeService() async {
final service = FlutterBackgroundService();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
notificationChannelId,
'ProxiBuy Background Service',
description: 'This channel is used for location service notifications.',
importance: Importance.low,
);
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
isForegroundMode: true,
autoStart: true,
notificationChannelId: notificationChannelId,
initialNotificationTitle: 'ProxiBuy فعال است',
initialNotificationContent: 'در حال جستجو برای تخفیف‌های اطراف شما...',
foregroundServiceNotificationId: notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
),
);
}

View File

@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/services/mqtt_service.dart';
const notificationChannelId = 'proxibuy_foreground_service';
const notificationId = 888;
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final MqttService mqttService = MqttService();
const storage = FlutterSecureStorage();
if (service is AndroidServiceInstance) {
service.setForegroundNotificationInfo(
title: "ProxiBuy فعال است",
content: "در حال یافتن تخفیف های اطراف شما...",
);
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on('stopService').listen((event) {
service.stopSelf();
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service is running and sending location...");
var locationStatus = await Permission.location.status;
var locationAlwaysStatus = await Permission.locationAlways.status;
if (!locationStatus.isGranted || !locationAlwaysStatus.isGranted) {
debugPrint("Background Service: Permissions not granted. Task skipped.");
return;
}
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final token = await storage.read(key: 'accessToken');
final userID = await storage.read(key: 'userID');
if (token != null && token.isNotEmpty && userID != null) {
if (!mqttService.isConnected) {
await mqttService.connect(token);
}
if (mqttService.isConnected) {
final payload = {"userID": userID, "lat": position.latitude, "lng": position.longitude};
mqttService.publish("proxybuy/sendGps", payload);
debugPrint("Background Service: GPS sent successfully.");
}
}
});
}
Future<void> initializeService() async {
final service = FlutterBackgroundService();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
notificationChannelId,
'ProxiBuy Background Service',
description: 'This channel is used for location service notifications.',
importance: Importance.low,
);
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
isForegroundMode: true,
autoStart: true,
notificationChannelId: notificationChannelId,
initialNotificationTitle: 'ProxiBuy فعال است',
initialNotificationContent: 'در حال جستجو برای تخفیف‌های اطراف شما...',
foregroundServiceNotificationId: notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
),
);
}

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_client.dart';
@ -10,18 +9,21 @@ class MqttService {
MqttServerClient? client;
final String server = '5.75.197.180';
final int port = 1883;
final StreamController<Map<String, dynamic>> _messageStreamController =
StreamController.broadcast();
final StreamController<Map<String, dynamic>> _messageStreamController = StreamController.broadcast();
Stream<Map<String, dynamic>> get messages => _messageStreamController.stream;
bool get isConnected {
return client?.connectionStatus?.state == MqttConnectionState.connected;
Completer<Map<String, dynamic>>? _firstMessageCompleter;
bool get isConnected => client?.connectionStatus?.state == MqttConnectionState.connected;
Future<Map<String, dynamic>> awaitFirstMessage() {
_firstMessageCompleter = Completer<Map<String, dynamic>>();
return _firstMessageCompleter!.future;
}
Future<void> connect(String token) async {
final String clientId =
'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
final String username = 'ignored';
final String password = token;
@ -31,10 +33,6 @@ class MqttService {
client!.autoReconnect = true;
client!.setProtocolV311();
debugPrint('--- [MQTT] Attempting to connect...');
debugPrint('--- [MQTT] Server: $server:$port');
debugPrint('--- [MQTT] ClientID: $clientId');
final connMessage = MqttConnectMessage()
.withClientIdentifier(clientId)
.startClean()
@ -46,86 +44,57 @@ class MqttService {
debugPrint('✅ [MQTT] Connected successfully.');
client!.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
final String payload =
MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
debugPrint('<<<<< [MQTT] Received Data <<<<<');
debugPrint('<<<<< [MQTT] Topic: ${c[0].topic}');
debugPrint('<<<<< [MQTT] Payload as String: $payload');
debugPrint('<<<<< ======================== <<<<<');
final String payload = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
try {
final Map<String, dynamic> jsonPayload = json.decode(payload);
_messageStreamController.add(jsonPayload);
if (!_messageStreamController.isClosed) {
_messageStreamController.add(jsonPayload);
}
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.complete(jsonPayload);
}
} catch (e) {
debugPrint("❌ [MQTT] Error decoding received JSON: $e");
debugPrint("❌ [MQTT] Error decoding JSON: $e");
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.completeError(e);
}
}
});
};
client!.onDisconnected = () {
debugPrint('❌ [MQTT] Disconnected.');
};
client!.onAutoReconnect = () {
debugPrint('↪️ [MQTT] Auto-reconnecting...');
};
client!.onAutoReconnected = () {
debugPrint('✅ [MQTT] Auto-reconnected successfully.');
};
client!.onSubscribed = (String topic) {
debugPrint('✅ [MQTT] Subscribed to topic: $topic');
};
client!.pongCallback = () {
debugPrint('🏓 [MQTT] Ping response received');
};
client!.onDisconnected = () => debugPrint('❌ [MQTT] Disconnected.');
client!.onSubscribed = (String topic) => debugPrint('✅ [MQTT] Subscribed to topic: $topic');
try {
await client!.connect();
} on NoConnectionException catch (e) {
debugPrint('❌ [MQTT] Connection failed - No Connection Exception: $e');
client?.disconnect();
} on SocketException catch (e) {
debugPrint('❌ [MQTT] Connection failed - Socket Exception: $e');
client?.disconnect();
} catch (e) {
debugPrint('❌ [MQTT] Connection failed - General Exception: $e');
debugPrint('❌ [MQTT] Connection failed: $e');
client?.disconnect();
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.completeError(e);
}
}
}
void subscribe(String topic) {
if (isConnected) {
client?.subscribe(topic, MqttQos.atLeastOnce);
} else {
debugPrint("⚠️ [MQTT] Cannot subscribe. Client is not connected.");
}
}
void publish(String topic, Map<String, dynamic> message) {
if (isConnected) {
final builder = MqttClientPayloadBuilder();
final payloadString = json.encode(message);
builder.addString(payloadString);
debugPrint('>>>>> [MQTT] Publishing Data >>>>>');
debugPrint('>>>>> [MQTT] Topic: $topic');
debugPrint('>>>>> [MQTT] Payload: $payloadString');
debugPrint('>>>>> ======================= >>>>>');
builder.addString(json.encode(message));
client?.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
} else {
debugPrint("⚠️ [MQTT] Cannot publish. Client is not connected.");
}
}
void dispose() {
debugPrint("--- [MQTT] Disposing MQTT Service.");
_messageStreamController.close();
client?.disconnect();
_messageStreamController.close();
}
}

View File

@ -11,7 +11,10 @@ import connectivity_plus
import file_selector_macos
import firebase_auth
import firebase_core
import firebase_crashlytics
import firebase_messaging
import firebase_storage
import flutter_local_notifications
import flutter_localization
import flutter_secure_storage_macos
import geolocator_apple
@ -28,7 +31,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))

View File

@ -521,6 +521,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.24.1"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1"
url: "https://pub.dev"
source: hosted
version: "4.3.10"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb"
url: "https://pub.dev"
source: hosted
version: "3.8.10"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
firebase_storage:
dependency: "direct main"
description:
@ -566,6 +606,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_background_service_android:
dependency: "direct main"
description:
name: flutter_background_service_android
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
url: "https://pub.dev"
source: hosted
version: "6.3.1"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.dev"
source: hosted
version: "5.1.2"
flutter_bloc:
dependency: "direct main"
description:
@ -614,6 +686,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae"
url: "https://pub.dev"
source: hosted
version: "19.4.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
url: "https://pub.dev"
source: hosted
version: "1.0.2"
flutter_localization:
dependency: "direct main"
description:
@ -1121,10 +1225,10 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.0+1"
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
@ -1466,6 +1570,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.5"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
timing:
dependency: transitive
description:
@ -1642,6 +1754,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: "746a50c535af15b6dc225abbd9b52ab272bcd292c535a104c54b5bc02609c38a"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
xdg_directories:
dependency: transitive
description:

View File

@ -42,7 +42,7 @@ dependencies:
flutter_gen: ^5.10.0
country_picker: ^2.0.27
geolocator: ^14.0.1
permission_handler: ^12.0.0+1
permission_handler: ^12.0.1
cached_network_image: ^3.4.1
collection: ^1.19.1
shared_preferences: ^2.5.3
@ -62,6 +62,12 @@ dependencies:
dart_jsonwebtoken: ^3.2.0
audioplayers: ^6.5.0
intl: ^0.19.0
flutter_background_service: ^5.1.0
flutter_background_service_android: ^6.3.1
flutter_local_notifications: ^19.4.0
workmanager: ^0.7.0
firebase_messaging: ^15.2.10
firebase_crashlytics: ^4.3.10
dev_dependencies:
flutter_test:

View File

@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)