background-activity #1

Open
Mr.Jebelli wants to merge 6 commits from background-activity into main
7 changed files with 226 additions and 164 deletions
Showing only changes of commit 608222e8a3 - Show all commits

View File

@ -11,7 +11,7 @@ plugins {
android { android {
namespace = "com.example.proxibuy" namespace = "com.example.proxibuy"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true

View File

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View File

@ -13,7 +13,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
late final Dio _dio; late final Dio _dio;
final _storage = const FlutterSecureStorage(); final _storage = const FlutterSecureStorage();
AuthBloc() : super(AuthInitial()) { AuthBloc() : super(AuthUnknown()) {
_dio = Dio(); _dio = Dio();
_dio.interceptors.add( _dio.interceptors.add(
LogInterceptor( LogInterceptor(
@ -44,6 +44,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
} }
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async { Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {

View File

@ -7,6 +7,8 @@ abstract class AuthState extends Equatable {
List<Object?> get props => []; List<Object?> get props => [];
} }
class AuthUnknown extends AuthState {}
class AuthInitial extends AuthState {} class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -130,7 +131,7 @@ class _NotificationPreferencesPageState
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
body: BlocListener<NotificationPreferencesBloc, body: BlocConsumer<NotificationPreferencesBloc,
NotificationPreferencesState>( NotificationPreferencesState>(
listener: (context, state) async { listener: (context, state) async {
if (state.submissionSuccess) { if (state.submissionSuccess) {
@ -176,7 +177,10 @@ class _NotificationPreferencesPageState
); );
} }
}, },
child: Padding( builder: (context, state) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -212,9 +216,8 @@ class _NotificationPreferencesPageState
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Expanded( Expanded(
child: BlocBuilder<NotificationPreferencesBloc, child: Builder(
NotificationPreferencesState>( builder: (context) {
builder: (context, state) {
if (state.categories.isEmpty && state.isLoading) { if (state.categories.isEmpty && state.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@ -258,18 +261,13 @@ class _NotificationPreferencesPageState
}, },
), ),
), ),
BlocBuilder<NotificationPreferencesBloc, if (state.selectedCategoryIds.isNotEmpty)
NotificationPreferencesState>( SizedBox(
builder: (context, state) {
final areCategoriesSelected =
state.selectedCategoryIds.isNotEmpty;
if (areCategoriesSelected) {
return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: !state.isLoading onPressed: state.isLoading
? () async { ? null
: () async {
final bloc = final bloc =
context.read<NotificationPreferencesBloc>(); context.read<NotificationPreferencesBloc>();
@ -288,21 +286,17 @@ class _NotificationPreferencesPageState
selectedCategoryNames); selectedCategoryNames);
bloc.add(SubmitPreferences()); bloc.add(SubmitPreferences());
} },
: null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm, backgroundColor: AppColors.confirm,
foregroundColor: Colors.white, foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey, disabledBackgroundColor: Colors.grey.withOpacity(0.5),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
), ),
child: state.isLoading child: const Text(
? const CircularProgressIndicator(
color: Colors.white)
: const Text(
'اعمال', 'اعمال',
style: TextStyle( style: TextStyle(
fontFamily: 'Dana', fontFamily: 'Dana',
@ -311,16 +305,23 @@ class _NotificationPreferencesPageState
), ),
), ),
), ),
);
} else {
return const SizedBox.shrink();
}
},
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
), ),
), ),
if (state.isLoading)
Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
],
);
},
), ),
); );
} }

View File

@ -1,4 +1,3 @@
// lib/presentation/pages/offers_page.dart
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -205,6 +204,39 @@ class _OffersPageState extends State<OffersPage> {
} }
} }
Future<void> _handleRefresh() {
final completer = Completer<void>();
final service = FlutterBackgroundService();
final timeout = Timer(const Duration(seconds: 20), () {
if (!completer.isCompleted) {
completer.completeError('Request timed out.');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Request timed out. Please try again.')),
);
}
}
});
final StreamSubscription<Map<String, dynamic>?> subscription =
service.on('update').listen((event) {
if (!completer.isCompleted) {
completer.complete();
}
});
completer.future.whenComplete(() {
subscription.cancel();
timeout.cancel();
});
service.invoke('force_refresh');
return completer.future;
}
Widget _buildFavoriteCategoriesSection() { Widget _buildFavoriteCategoriesSection() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
@ -220,7 +252,7 @@ class _OffersPageState extends State<OffersPage> {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await Navigator.of(context).push<bool>( final result = await Navigator.of(context).push<bool>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const NotificationPreferencesPage( builder: (context) => const NotificationPreferencesPage(
loadFavoritesOnStart: true, loadFavoritesOnStart: true,
@ -234,7 +266,11 @@ class _OffersPageState extends State<OffersPage> {
.read<NotificationPreferencesBloc>() .read<NotificationPreferencesBloc>()
.add(ResetSubmissionStatus()); .add(ResetSubmissionStatus());
_loadPreferences(); await _loadPreferences();
if (result == true) {
_handleRefresh();
}
}, },
child: Row( child: Row(
children: [ children: [
@ -362,7 +398,10 @@ class _OffersPageState extends State<OffersPage> {
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
body: SingleChildScrollView( body: RefreshIndicator(
onRefresh: _handleRefresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -370,11 +409,13 @@ class _OffersPageState extends State<OffersPage> {
OffersView( OffersView(
isGpsEnabled: _isGpsEnabled, isGpsEnabled: _isGpsEnabled,
isConnectedToInternet: _isConnectedToInternet, isConnectedToInternet: _isConnectedToInternet,
selectedCategories: _selectedCategories,
), ),
], ],
), ),
), ),
), ),
),
); );
} }
} }
@ -382,11 +423,13 @@ class _OffersPageState extends State<OffersPage> {
class OffersView extends StatelessWidget { class OffersView extends StatelessWidget {
final bool isGpsEnabled; final bool isGpsEnabled;
final bool isConnectedToInternet; final bool isConnectedToInternet;
final List<String> selectedCategories;
const OffersView({ const OffersView({
super.key, super.key,
required this.isGpsEnabled, required this.isGpsEnabled,
required this.isConnectedToInternet, required this.isConnectedToInternet,
required this.selectedCategories,
}); });
@override @override
@ -418,7 +461,13 @@ class OffersView extends StatelessWidget {
} }
if (state is OffersLoadSuccess) { if (state is OffersLoadSuccess) {
if (state.offers.isEmpty) { final filteredOffers = selectedCategories.isEmpty
? state.offers
: state.offers
.where((offer) => selectedCategories.contains(offer.category))
.toList();
if (filteredOffers.isEmpty) {
return const SizedBox( return const SizedBox(
height: 300, height: 300,
child: Center( child: Center(
@ -434,7 +483,7 @@ class OffersView extends StatelessWidget {
} }
final groupedOffers = groupBy( final groupedOffers = groupBy(
state.offers, filteredOffers,
(OfferModel offer) => offer.category, (OfferModel offer) => offer.category,
); );
final categories = groupedOffers.keys.toList(); final categories = groupedOffers.keys.toList();

View File

@ -30,13 +30,7 @@ void onStart(ServiceInstance service) async {
}); });
} }
mqttService.messages.listen((data) { Future<void> sendGpsData() async {
service.invoke('update', {'offers': data});
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service: Sending location...");
var locationStatus = await Permission.location.status; var locationStatus = await Permission.location.status;
if (!locationStatus.isGranted) { if (!locationStatus.isGranted) {
debugPrint("Background Service: Location permission not granted."); debugPrint("Background Service: Location permission not granted.");
@ -69,6 +63,20 @@ void onStart(ServiceInstance service) async {
} catch (e) { } catch (e) {
debugPrint("❌ Background Service Error: $e"); debugPrint("❌ Background Service Error: $e");
} }
}
mqttService.messages.listen((data) {
service.invoke('update', {'offers': data});
});
service.on('force_refresh').listen((event) async {
debugPrint("✅ Background Service: Received force_refresh event.");
await sendGpsData();
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service: Sending location via periodic timer...");
await sendGpsData();
}); });
} }