Chepuhagram/lib/main.dart

542 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:chepuhagram/domain/services/crypto_service.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:convert';
import 'package:chepuhagram/data/datasources/local_db_service.dart';
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 'package:path/path.dart' as p;
import 'presentation/screens/splash_screen.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'presentation/screens/call_screen.dart';
import 'package:flutter_callkit_incoming/entities/entities.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';
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);
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);
}
_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');
}
}
bool firebaseInitialized = false;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
sqfliteFfiInit();
// ИСПОЛЬЗУЕМ СУППОРТ-ДИРЕКТОРИЮ (App Data / Roaming) вместо Документов
final appSupportDir = await getApplicationSupportDirectory();
// Создаем подпапку внутри AppData, там Windows никогда не выдаст ошибку 2
final sqfliteDbPath = p.join(appSupportDir.path, 'sqflite_databases');
await Directory(sqfliteDbPath).create(recursive: true);
// Привязываем фабрику sqflite к новому безопасному пути
databaseFactory = databaseFactoryFfi;
await databaseFactory.setDatabasesPath(sqfliteDbPath);
print('Безопасный путь для SQFlite на Windows: $sqfliteDbPath');
}
try {
print('Initializing LocalDbService in main...');
final db = LocalDbService();
print('База данных сохранена по пути: ${db.connection.connectionData}');
print('LocalDbService initialized successfully.');
} catch (e, st) {
print('LocalDbService init failed in main: $e');
print(st);
}
if (Platform.isAndroid || Platform.isIOS) {
await Firebase.initializeApp();
firebaseInitialized = true;
initialMessage = await FirebaseMessaging.instance.getInitialMessage();
print('Initial message from main(): $initialMessage');
} else {
print('Skipping Firebase initialization on desktop.');
}
// Сохраняем информацию в 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);
}
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const WindowsInitializationSettings initializationSettingsWindows =
WindowsInitializationSettings(
appName: 'Chepuhagram',
appUserModelId: 'ru.ArturKarasevich.Chepuhagram',
guid: '6c0af055-e0b5-4f10-9aed-c12dc078f949',
);
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
windows: initializationSettingsWindows,
);
await flutterLocalNotificationsPlugin.initialize(
settings: 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);
if (firebaseInitialized) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
}
}
if (Platform.isAndroid || Platform.isIOS) {
initCallkitListener();
} else {
print('Skipping CallKit listener on desktop platform.');
}
runApp(
MultiProvider(
providers: [
Provider(create: (_) => CryptoService()),
Provider(create: (_) => SocketService()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
Provider(create: (_) => SocketService()),
ChangeNotifierProvider(
create: (context) => ContactProvider(context.read<CryptoService>()),
),
],
child: const MyApp(),
),
);
}
void initCallkitListener() {
if (!(Platform.isAndroid || Platform.isIOS)) {
print('Skipping CallKit event listener on non-mobile platform.');
return;
}
try {
FlutterCallkitIncoming.onEvent.listen((event) {
if (event == null) return;
switch (event.event) {
case Event.actionCallIncoming:
// Звонок получен, но CallKit уже показал экран.
// Здесь можно логировать или обновить статус в БД.
print("Incoming call: ${event.body['id']}");
break;
case Event.actionCallStart:
// Исходящий звонок начат
break;
case Event.actionCallAccept:
// ПОЛЬЗОВАТЕЛЬ НАЖАЛ "ПРИНЯТЬ"
// 1. Уведомляем сервер (чтобы другая сторона узнала о начале звонка)
SocketService().sendMessage({
"type": "call_accept",
"call_id": event.body['id'],
});
// 2. Переходим на экран звонка
navigatorKey.currentState?.push(
MaterialPageRoute(
builder: (_) => CallScreen(
callId: event.body['id'],
isIncoming: true,
callerName: event.body['nameCaller'] ?? 'Unknown',
onAccept: () {},
onHangup: () => _handleHangupGlobal(event.body['id']),
),
),
);
break;
case Event.actionCallDecline:
// ПОЛЬЗОВАТЕЛЬ НАЖАЛ "ОТКЛОНИТЬ"
SocketService().sendMessage({
"type": "decline",
"call_id": event.body['id'],
});
break;
case Event.actionCallEnded:
case Event.actionCallTimeout:
// Звонок завершен или пропущен
print("Call ended or timeout");
break;
default:
print("Event unhandled: ${event.event}");
break;
}
});
} catch (e, st) {
print('CallKit listener initialization failed: $e');
print(st);
}
}
void _handleHangupGlobal(String callId) {
SocketService().sendMessage({"type": "hangup", "call_id": callId});
}
@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 WindowsInitializationSettings initializationSettingsWindows =
WindowsInitializationSettings(
appName: 'Chepuhagram',
appUserModelId: 'ru.ArturKarasevich.Chepuhagram',
guid: '6c0af055-e0b5-4f10-9aed-c12dc078f949',
);
final InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
windows: initializationSettingsWindows,
);
await flutterLocalNotificationsPlugin.initialize(
settings: initializationSettings,
onDidReceiveNotificationResponse: _onSelectNotification,
);
// 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()}';
}
final senderId = int.tryParse(
message.data['sender_id']?.toString() ?? '',
);
// 4. Показываем локальное уведомление
final String groupKey = 'ru.chepuhagram.app.$senderId';
await flutterLocalNotificationsPlugin.show(
id: senderId!,
title: '',
body: '',
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
'Messages',
'Новые сообщения',
groupKey: groupKey,
setAsGroupSummary: true,
importance: Importance.high,
priority: Priority.high,
groupAlertBehavior: GroupAlertBehavior.all,
),
),
);
await flutterLocalNotificationsPlugin.show(
id: message.hashCode,
title: message.data['username'] ?? 'Unknown',
body: notificationText,
notificationDetails: NotificationDetails(
android: AndroidNotificationDetails(
'chat_id',
'Messages',
groupKey: groupKey,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
),
),
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(),
);
}
}