import 'dart:io'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../config/app_phonebook_names.dart'; import '../utils/logger.dart'; class DeviceContactsService { DeviceContactsService._(); static const _deniedKey = 'phonebook_denied'; static final Map _byLast10 = {}; static final Map _phonebook = {}; static Map get phonebook => Map.unmodifiable(_phonebook); static bool _loaded = false; static bool get _supported => Platform.isAndroid || Platform.isIOS; static bool get isLoaded => _loaded; static int get knownNumbers => _byLast10.length; static String? _last10(String raw) { final digits = raw.replaceAll(RegExp(r'[^\d]'), ''); if (digits.length < 10) return null; return digits.substring(digits.length - 10); } static String? nameForPhone(int phone) { if (!AppPhonebookNames.current.value) return null; if (_byLast10.isEmpty) return null; final key = _last10(phone.toString()); if (key == null) return null; final name = _byLast10[key]; if (name == null || name.trim().isEmpty) return null; return name.trim(); } static Future hasPermission() async { if (!_supported) return false; try { return await Permission.contacts.isGranted; } catch (e) { logger.w('Телефонная книга: не удалось прочитать статус разрешения: $e'); return false; } } static Future loadFromStartup() async { if (_loaded || !_supported) return; if (!AppPhonebookNames.current.value) return; if (!await hasPermission()) return; await _forgetDenial(); await _readBook(); } static Future ensureLoadedInteractive({ bool force = false, bool forContacts = false, }) async { if (!_supported) return false; if (!forContacts && !AppPhonebookNames.current.value) return false; if (_loaded && !force) return false; if (await hasPermission()) { await _forgetDenial(); return _readBook(); } final prefs = await SharedPreferences.getInstance(); if (!force && prefs.getBool(_deniedKey) == true) return false; final granted = await FlutterContacts.requestPermission(readonly: true); if (!granted) { await prefs.setBool(_deniedKey, true); return false; } await prefs.remove(_deniedKey); return _readBook(); } static Future reload() async { _loaded = false; _byLast10.clear(); _phonebook.clear(); return ensureLoadedInteractive(force: true); } static Future _forgetDenial() async { final prefs = await SharedPreferences.getInstance(); if (prefs.getBool(_deniedKey) == true) await prefs.remove(_deniedKey); } static Future _readBook() async { _loaded = false; _byLast10.clear(); _phonebook.clear(); try { FlutterContacts.config.includeNonVisibleOnAndroid = true; final contacts = await FlutterContacts.getContacts(withProperties: true); _byLast10.clear(); _phonebook.clear(); for (final contact in contacts) { final name = contact.displayName.trim(); for (final phone in contact.phones) { final normalized = normalizePhone(phone.number); if (normalized != null) { _phonebook.putIfAbsent( normalized, () => name.isEmpty ? normalized : name, ); } final key = _last10(phone.number); if (key != null) { _byLast10.putIfAbsent(key, () => name); } } } _loaded = true; logger.i( 'Телефонная книга: прочитано ${contacts.length} записей, ' '${_byLast10.length} номеров', ); return true; } catch (e) { logger.w('Телефонная книга: не удалось прочитать: $e'); return false; } } static String? normalizePhone(String raw) { var digits = raw.replaceAll(RegExp(r'[^\d]'), ''); if (digits.length == 11 && digits.startsWith('8')) { digits = '7${digits.substring(1)}'; } if (digits.length < 11 || digits.length > 15) return null; return '+$digits'; } }