377 lines
14 KiB
Dart
377 lines
14 KiB
Dart
import 'dart:io';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'data/datasources/ws_client.dart';
|
||
import 'logic/auth_provider.dart';
|
||
import 'logic/contact_provider.dart';
|
||
import 'core/theme_manager.dart';
|
||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||
import 'package:firebase_core/firebase_core.dart';
|
||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||
import 'dart:convert';
|
||
import 'package:chepuhagram/presentation/screens/chat_screen.dart';
|
||
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'presentation/screens/splash_screen.dart';
|
||
|
||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||
FlutterLocalNotificationsPlugin();
|
||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||
|
||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||
|
||
// Глобальная переменная для отслеживания текущего активного контакта в чате
|
||
int? currentActiveChatContactId;
|
||
|
||
// Глобальная переменная для хранения начального сообщения (при запуске из уведомления)
|
||
RemoteMessage? initialMessage;
|
||
|
||
// Ключ для SharedPreferences
|
||
const String _notificationLaunchKey = 'notification_launch_data';
|
||
// Защита от повторной обработки одного и того же payload при следующих запусках по иконке
|
||
const String _lastHandledNotificationLaunchPayloadKey =
|
||
'notification_last_handled_payload';
|
||
|
||
Future<void> _onSelectNotification(
|
||
NotificationResponse notificationResponse,
|
||
) async {
|
||
final payload = notificationResponse.payload;
|
||
if (payload != null) {
|
||
try {
|
||
final data = jsonDecode(payload);
|
||
final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
|
||
if (senderId != null) {
|
||
print('Notification selected, payload sender_id=$senderId');
|
||
|
||
final context = navigatorKey.currentContext;
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final canonicalPayload = jsonEncode(data);
|
||
|
||
// Важно: не сохраняем payload в SharedPreferences, если можем сразу перейти в чат.
|
||
// Иначе при следующем обычном запуске (по иконке) останется "хвост" и приложение
|
||
// будет снова автопереходить в чат.
|
||
if (context == null) {
|
||
final lastHandled = prefs.getString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
);
|
||
if (lastHandled != canonicalPayload) {
|
||
await prefs.setString(_notificationLaunchKey, canonicalPayload);
|
||
await prefs.setString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
canonicalPayload,
|
||
);
|
||
}
|
||
print(
|
||
'Navigator context is null, saved notification payload to SharedPreferences',
|
||
);
|
||
} else {
|
||
await prefs.remove(_notificationLaunchKey);
|
||
}
|
||
|
||
// Navigate to chat with this contact (if context is ready)
|
||
_navigateToChat(senderId);
|
||
} else {
|
||
print(
|
||
'Notification payload has invalid sender_id: ${data['sender_id']}',
|
||
);
|
||
}
|
||
} catch (e) {
|
||
print('Error parsing notification payload: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
void _navigateToChat(int senderId) {
|
||
print('Navigating to chat with senderId: $senderId');
|
||
final context = navigatorKey.currentContext;
|
||
if (context != null) {
|
||
final contactProvider = Provider.of<ContactProvider>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
|
||
// Check if contacts are loaded
|
||
if (contactProvider.contacts.isEmpty) {
|
||
print('Contacts not loaded yet, navigating to contacts screen first');
|
||
// Navigate to contacts screen and pass the senderId to navigate to chat later
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (_) => ContactsScreen(targetChatId: senderId),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final contact = contactProvider.contacts.firstWhere(
|
||
(c) => c.id == senderId,
|
||
orElse: () => throw Exception('Contact not found'),
|
||
);
|
||
print('Found contact: ${contact.username}, navigating to chat');
|
||
currentActiveChatContactId = senderId; // Устанавливаем активный чат
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||
);
|
||
} catch (e) {
|
||
print(
|
||
'Contact with id $senderId not found, navigating to contacts screen',
|
||
);
|
||
// Contact not found, go to contacts screen
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||
);
|
||
}
|
||
} else {
|
||
print('Navigator context is null');
|
||
}
|
||
}
|
||
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
await Firebase.initializeApp();
|
||
|
||
// Проверяем, было ли приложение запущено из уведомления
|
||
// Добавляем небольшую задержку, чтобы Firebase полностью инициализировался
|
||
await Future.delayed(const Duration(milliseconds: 500));
|
||
initialMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||
print('Initial message from main() after delay: $initialMessage');
|
||
// Сохраняем информацию в SharedPreferences для надежности
|
||
final prefs = await SharedPreferences.getInstance();
|
||
if (initialMessage != null) {
|
||
print('App launched from notification: ${initialMessage!.data}');
|
||
print('Message type: ${initialMessage!.data['type']}');
|
||
print('Sender ID: ${initialMessage!.data['sender_id']}');
|
||
|
||
final payloadString = jsonEncode(initialMessage!.data);
|
||
final lastHandled = prefs.getString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
);
|
||
if (lastHandled != payloadString) {
|
||
// Сохраняем данные уведомления
|
||
await prefs.setString(_notificationLaunchKey, payloadString);
|
||
await prefs.setString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
payloadString,
|
||
);
|
||
print('Saved notification data to SharedPreferences');
|
||
} else {
|
||
print('InitialMessage payload already handled earlier, skipping');
|
||
}
|
||
} else {
|
||
print('No initial message - app launched normally');
|
||
// Очищаем сохраненные данные, если приложение запущено нормально
|
||
await prefs.remove(_notificationLaunchKey);
|
||
}
|
||
|
||
// Initialize local notifications
|
||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||
final InitializationSettings initializationSettings = InitializationSettings(
|
||
android: initializationSettingsAndroid,
|
||
);
|
||
await flutterLocalNotificationsPlugin.initialize(
|
||
initializationSettings,
|
||
onDidReceiveNotificationResponse: _onSelectNotification,
|
||
);
|
||
|
||
// Если приложение было запущено из локального уведомления, сохраним payload
|
||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
||
.getNotificationAppLaunchDetails();
|
||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||
final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
|
||
print('App launched from local notification, payload: $payload');
|
||
if (payload != null && payload.isNotEmpty) {
|
||
try {
|
||
final lastHandled = prefs.getString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
);
|
||
if (lastHandled != payload) {
|
||
final data = jsonDecode(payload);
|
||
await prefs.setString(_notificationLaunchKey, jsonEncode(data));
|
||
await prefs.setString(
|
||
_lastHandledNotificationLaunchPayloadKey,
|
||
payload,
|
||
);
|
||
print('Saved local notification launch payload to SharedPreferences');
|
||
} else {
|
||
print('Local notification payload already handled earlier, skipping');
|
||
}
|
||
} catch (e) {
|
||
print('Failed to save notification launch payload: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create notification channel for Android 8+
|
||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||
'chat_id', // id
|
||
'Messages', // title
|
||
description: 'Chat messages notifications', // description
|
||
importance: Importance.high,
|
||
);
|
||
|
||
await flutterLocalNotificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin
|
||
>()
|
||
?.createNotificationChannel(channel);
|
||
|
||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||
|
||
runApp(
|
||
MultiProvider(
|
||
providers: [
|
||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||
ChangeNotifierProvider(create: (_) => ContactProvider()),
|
||
Provider(create: (_) => SocketService()),
|
||
],
|
||
child: const MyApp(),
|
||
),
|
||
);
|
||
}
|
||
|
||
@pragma('vm:entry-point')
|
||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||
print("Фоновый пуш получен: ${message.data}");
|
||
if (message.data['type'] == 'enc_message') {
|
||
try {
|
||
// Initialize notifications for background
|
||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||
const InitializationSettings initializationSettings =
|
||
InitializationSettings(android: initializationSettingsAndroid);
|
||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||
|
||
// Create notification channel
|
||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||
'chat_id',
|
||
'Messages',
|
||
description: 'Chat messages notifications',
|
||
importance: Importance.high,
|
||
);
|
||
|
||
await flutterLocalNotificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin
|
||
>()
|
||
?.createNotificationChannel(channel);
|
||
|
||
// Try to decrypt
|
||
String notificationText = 'New encrypted message';
|
||
try {
|
||
// 1. Инициализируем крипто-сервис
|
||
final crypto = CryptoService();
|
||
|
||
// 2. Достаем ключи (они должны быть в SecureStorage)
|
||
final myPrivKey = await crypto.getPrivateKey();
|
||
print('Private key retrieved: ${myPrivKey != null}');
|
||
if (myPrivKey == null) {
|
||
print('Private key not found, showing encrypted message');
|
||
notificationText =
|
||
'Encrypted message: ${message.data['content']?.substring(0, 50) ?? 'N/A'}...';
|
||
} else {
|
||
// 3. Расшифровываем
|
||
final sharedSecret = await crypto.deriveSharedSecret(
|
||
myPrivKey,
|
||
message.data['public_key'],
|
||
);
|
||
final decryptedText = await crypto.decryptMessage(
|
||
message.data['content'],
|
||
sharedSecret,
|
||
);
|
||
notificationText = decryptedText;
|
||
}
|
||
} catch (e) {
|
||
print('Decryption failed: $e');
|
||
notificationText = 'Failed to decrypt: ${e.toString()}';
|
||
}
|
||
|
||
// 4. Показываем локальное уведомление
|
||
await flutterLocalNotificationsPlugin.show(
|
||
message.hashCode,
|
||
message.data['username'] ?? 'Unknown',
|
||
notificationText,
|
||
const NotificationDetails(
|
||
android: AndroidNotificationDetails('chat_id', 'Messages'),
|
||
),
|
||
payload: jsonEncode({
|
||
'type': 'enc_message',
|
||
'sender_id': message.data['sender_id'],
|
||
'timestamp':
|
||
message.data['timestamp'] ?? DateTime.now().toIso8601String(),
|
||
}),
|
||
);
|
||
print('Notification shown successfully');
|
||
} catch (e) {
|
||
print('Error processing background message: $e');
|
||
}
|
||
} else {
|
||
print('Message type is not enc_message: ${message.data['type']}');
|
||
}
|
||
}
|
||
|
||
class MyApp extends StatefulWidget {
|
||
const MyApp({super.key});
|
||
|
||
@override
|
||
State<MyApp> createState() => _MyAppState();
|
||
}
|
||
|
||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
// Закрываем сокет, как только приложение сворачивается.
|
||
if (state == AppLifecycleState.paused ||
|
||
state == AppLifecycleState.inactive ||
|
||
state == AppLifecycleState.detached) {
|
||
try {
|
||
context.read<AuthProvider>().closeRealtime();
|
||
} catch (_) {}
|
||
return;
|
||
}
|
||
|
||
// На возврате в приложение — пробуем переподключиться (если есть токен).
|
||
if (state == AppLifecycleState.resumed) {
|
||
try {
|
||
context.read<AuthProvider>().initRealtime();
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final themeProvider = context.watch<ThemeProvider>();
|
||
return MaterialApp(
|
||
title: 'Chepuhagram',
|
||
debugShowCheckedModeBanner: false,
|
||
themeAnimationDuration: const Duration(milliseconds: 300),
|
||
themeAnimationCurve: Curves.easeInOut,
|
||
theme: themeProvider.themeData,
|
||
themeMode: themeProvider.themeMode,
|
||
navigatorKey: navigatorKey,
|
||
navigatorObservers: [routeObserver],
|
||
|
||
// Начальный экран
|
||
home: const SplashScreen(),
|
||
);
|
||
}
|
||
}
|