init
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'services/settings_service.dart';
|
||||
import 'services/api_service.dart';
|
||||
import 'services/sms_reader_service.dart';
|
||||
@@ -18,6 +19,18 @@ void main() async {
|
||||
apiService.configure(settings.serverUrl!, settings.token!);
|
||||
}
|
||||
|
||||
FlutterForegroundTask.initCommunicationPort();
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'sms_monitor_foreground',
|
||||
channelName: 'SMS Monitor Service',
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(),
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
eventAction: ForegroundTaskEventAction.nothing(),
|
||||
),
|
||||
);
|
||||
|
||||
runApp(const SmsMonitorApp());
|
||||
}
|
||||
|
||||
@@ -27,21 +40,19 @@ class SmsMonitorApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (ColorScheme? dynamicScheme) {
|
||||
final scheme = dynamicScheme?.harmonized() ??
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
final lightScheme = lightDynamic?.harmonized() ??
|
||||
ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.light);
|
||||
final darkScheme = darkDynamic?.harmonized() ??
|
||||
ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark);
|
||||
return MaterialApp(
|
||||
title: 'SMS Monitor',
|
||||
theme: ThemeData(
|
||||
colorScheme: scheme,
|
||||
colorScheme: lightScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: dynamicScheme?.harmonized() ??
|
||||
ColorScheme.fromSeed(
|
||||
seedColor: Colors.indigo,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
colorScheme: darkScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: settings.isConfigured ? const HomeScreen() : const LoginScreen(),
|
||||
|
||||
@@ -23,8 +23,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
selectedIndex: _index,
|
||||
onDestinationSelected: (i) => setState(() => _index = i),
|
||||
destinations: const [
|
||||
NavigationDestination(icon: Icon(Icons.sms_outlined), selectedIcon: Icon(Icons.sms), label: 'Messages'),
|
||||
NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
NavigationDestination(icon: Icon(Icons.sms_outlined), selectedIcon: Icon(Icons.sms), label: '短信'),
|
||||
NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: '设置'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../main.dart';
|
||||
import 'home_screen.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
@@ -10,30 +11,30 @@ class LoginScreen extends StatefulWidget {
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _serverCtrl = TextEditingController(text: 'http://192.168.1.100:3000');
|
||||
final _userCtrl = TextEditingController();
|
||||
final _passCtrl = TextEditingController();
|
||||
final _tokenCtrl = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
Future<void> _submit(bool register) async {
|
||||
Future<void> _connect() async {
|
||||
setState(() { _loading = true; _error = null; });
|
||||
|
||||
final url = _serverCtrl.text.trim();
|
||||
final user = _userCtrl.text.trim();
|
||||
final pass = _passCtrl.text.trim();
|
||||
final token = _tokenCtrl.text.trim();
|
||||
|
||||
try {
|
||||
apiService.configure(url, '');
|
||||
final token = register
|
||||
? await apiService.register(user, pass)
|
||||
: await apiService.login(user, pass);
|
||||
final valid = await apiService.verifyToken(token);
|
||||
if (!valid) {
|
||||
setState(() { _error = '令牌无效'; _loading = false; });
|
||||
return;
|
||||
}
|
||||
apiService.configure(url, token);
|
||||
await settings.setServerUrl(url);
|
||||
await settings.setToken(token);
|
||||
await apiService.registerDevice('Android');
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -61,55 +62,36 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
TextField(
|
||||
controller: _serverCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Server URL',
|
||||
hintText: 'http://your-server:3000',
|
||||
labelText: '服务器地址',
|
||||
hintText: 'http://服务器IP:3000',
|
||||
prefixIcon: Icon(Icons.dns_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _userCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Username',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _passCtrl,
|
||||
controller: _tokenCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
labelText: '访问令牌',
|
||||
prefixIcon: Icon(Icons.key),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (_) => _submit(false),
|
||||
onSubmitted: (_) => _connect(),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(_error!, style: TextStyle(color: colors.error)),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _loading ? null : () => _submit(true),
|
||||
child: const Text('Register'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: _loading ? null : () => _submit(false),
|
||||
child: _loading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('Sign In'),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: _loading ? null : _connect,
|
||||
child: _loading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('连接'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import '../main.dart';
|
||||
import 'login_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Settings')),
|
||||
appBar: AppBar(title: const Text('设置')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -22,11 +28,11 @@ class SettingsScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Server', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text('服务器', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text('URL: ${settings.serverUrl ?? "Not set"}',
|
||||
Text('地址: ${settings.serverUrl ?? "未设置"}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colors.onSurfaceVariant)),
|
||||
Text('Token: ${settings.token != null ? "Configured" : "Missing"}',
|
||||
Text('令牌: ${settings.token != null ? "已配置" : "未设置"}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colors.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
@@ -39,13 +45,24 @@ class SettingsScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Upload Mode', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text('上传模式', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
title: const Text('Auto Upload'),
|
||||
subtitle: const Text('Automatically upload new incoming SMS'),
|
||||
title: const Text('自动上传'),
|
||||
subtitle: Text(
|
||||
settings.autoUpload
|
||||
? '后台服务运行中,自动上传新短信'
|
||||
: '自动上传新收到的短信',
|
||||
style: TextStyle(color: settings.autoUpload ? colors.primary : null),
|
||||
),
|
||||
value: settings.autoUpload,
|
||||
onChanged: (v) => settings.setAutoUpload(v),
|
||||
onChanged: (v) {
|
||||
settings.setAutoUpload(v);
|
||||
if (!v) {
|
||||
FlutterForegroundTask.stopService();
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
@@ -57,14 +74,16 @@ class SettingsScreen extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
FlutterForegroundTask.stopService();
|
||||
settings.setToken('');
|
||||
settings.setAutoUpload(false);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
(_) => false,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('Logout'),
|
||||
label: const Text('断开连接'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: colors.error),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import '../models/sms_message.dart';
|
||||
import '../main.dart';
|
||||
import '../services/sms_foreground_handler.dart';
|
||||
|
||||
class SmsListScreen extends StatefulWidget {
|
||||
const SmsListScreen({super.key});
|
||||
@@ -24,7 +26,6 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_uploadedKeys = settings.getUploadedKeys();
|
||||
_requestPermission();
|
||||
}
|
||||
|
||||
@@ -32,16 +33,35 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
final status = await Permission.sms.request();
|
||||
if (mounted) {
|
||||
setState(() => _hasSmsPermission = status.isGranted);
|
||||
if (status.isGranted) _loadSms();
|
||||
if (status.isGranted) {
|
||||
_loadSms();
|
||||
if (settings.autoUpload) _startAutoUpload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSms() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
_messages = await smsReaderService.queryAllSms();
|
||||
final local = await smsReaderService.queryAllSms();
|
||||
|
||||
Set<String> serverKeys = {};
|
||||
try {
|
||||
final serverMsgs = await apiService.fetchServerSms();
|
||||
serverKeys = serverMsgs.map((m) {
|
||||
final phone = m['phone_number'] as String? ?? '';
|
||||
final content = m['content'] as String? ?? '';
|
||||
final date = m['sms_date'] as String? ?? '';
|
||||
return '$phone|$content|$date';
|
||||
}).toSet();
|
||||
} catch (_) {}
|
||||
|
||||
setState(() {
|
||||
_messages = local;
|
||||
_uploadedKeys = serverKeys;
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
setState(() => _loading = false);
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
bool _isUploaded(SmsMessage m) => _uploadedKeys.contains(_dedupKey(m));
|
||||
@@ -57,19 +77,20 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
if (toUpload.isEmpty) return;
|
||||
try {
|
||||
await apiService.uploadSms(toUpload);
|
||||
final newKeys = toUpload.map(_dedupKey).toList();
|
||||
settings.addUploadedKeys(newKeys);
|
||||
_uploadedKeys = settings.getUploadedKeys();
|
||||
_selected.clear();
|
||||
final newKeys = toUpload.map(_dedupKey).toSet();
|
||||
setState(() {
|
||||
_uploadedKeys = _uploadedKeys.union(newKeys);
|
||||
_selected.clear();
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uploaded ${toUpload.length} messages')),
|
||||
SnackBar(content: Text('已上传 ${toUpload.length} 条短信')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Upload failed: $e')),
|
||||
SnackBar(content: Text('上传失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,50 +101,68 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
if (toUpload.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No new messages to upload')),
|
||||
const SnackBar(content: Text('没有新短信需要上传')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final count = await apiService.uploadSms(toUpload);
|
||||
final newKeys = toUpload.map(_dedupKey).toList();
|
||||
settings.addUploadedKeys(newKeys);
|
||||
_uploadedKeys = settings.getUploadedKeys();
|
||||
await apiService.uploadSms(toUpload);
|
||||
final newKeys = toUpload.map(_dedupKey).toSet();
|
||||
setState(() {
|
||||
_uploadedKeys = _uploadedKeys.union(newKeys);
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uploaded $count messages')),
|
||||
SnackBar(content: Text('已上传 ${toUpload.length} 条短信')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Upload failed: $e')),
|
||||
SnackBar(content: Text('上传失败: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleAutoUpload() {
|
||||
Future<void> _toggleAutoUpload() async {
|
||||
final newVal = !settings.autoUpload;
|
||||
settings.setAutoUpload(newVal);
|
||||
|
||||
if (newVal) {
|
||||
// Request notification permission for foreground service
|
||||
await Permission.notification.request();
|
||||
_startForegroundService();
|
||||
_startAutoUpload();
|
||||
} else {
|
||||
_stopForegroundService();
|
||||
_smsSub?.cancel();
|
||||
_smsSub = null;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _startForegroundService() {
|
||||
FlutterForegroundTask.startService(
|
||||
notificationTitle: 'SMS Monitor',
|
||||
notificationText: '正在监听新短信...',
|
||||
callback: startCallback,
|
||||
);
|
||||
}
|
||||
|
||||
void _stopForegroundService() {
|
||||
FlutterForegroundTask.stopService();
|
||||
}
|
||||
|
||||
void _startAutoUpload() {
|
||||
_smsSub = smsReaderService.listenToIncoming().listen((sms) async {
|
||||
try {
|
||||
await apiService.uploadSms([sms]);
|
||||
settings.addUploadedKey(_dedupKey(sms));
|
||||
_uploadedKeys = settings.getUploadedKeys();
|
||||
_messages.insert(0, sms);
|
||||
if (mounted) setState(() {});
|
||||
setState(() {
|
||||
_uploadedKeys.add(_dedupKey(sms));
|
||||
_messages.insert(0, sms);
|
||||
});
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
@@ -140,10 +179,10 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Messages'),
|
||||
title: const Text('短信'),
|
||||
actions: [
|
||||
if (_selected.isNotEmpty)
|
||||
IconButton(icon: const Icon(Icons.cloud_upload), onPressed: _uploadSelected, tooltip: 'Upload selected'),
|
||||
IconButton(icon: const Icon(Icons.cloud_upload), onPressed: _uploadSelected, tooltip: '上传选中'),
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _hasSmsPermission ? _loadSms : _requestPermission),
|
||||
],
|
||||
),
|
||||
@@ -152,16 +191,16 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('SMS permission required'),
|
||||
const Text('需要短信权限'),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(onPressed: _requestPermission, child: const Text('Grant')),
|
||||
FilledButton(onPressed: _requestPermission, child: const Text('授权')),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _messages.isEmpty
|
||||
? const Center(child: Text('No messages'))
|
||||
? const Center(child: Text('暂无短信'))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadSms,
|
||||
child: ListView.builder(
|
||||
@@ -219,7 +258,7 @@ class _SmsListScreenState extends State<SmsListScreen> {
|
||||
FloatingActionButton(
|
||||
heroTag: 'upload',
|
||||
onPressed: _uploadAll,
|
||||
tooltip: 'Upload all',
|
||||
tooltip: '全部上传',
|
||||
child: const Icon(Icons.cloud_upload),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,24 +16,23 @@ class ApiService {
|
||||
if (_token != null) 'Authorization': 'Bearer $_token',
|
||||
};
|
||||
|
||||
Future<String> login(String username, String password) async {
|
||||
Future<bool> verifyToken(String token) async {
|
||||
final res = await http.post(
|
||||
Uri.parse('$_baseUrl/api/auth/login'),
|
||||
Uri.parse('$_baseUrl/api/auth/verify'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'username': username, 'password': password}),
|
||||
body: jsonEncode({'token': token}),
|
||||
);
|
||||
if (res.statusCode != 200) throw Exception(_error(res));
|
||||
return jsonDecode(res.body)['token'];
|
||||
return res.statusCode == 200;
|
||||
}
|
||||
|
||||
Future<String> register(String username, String password) async {
|
||||
final res = await http.post(
|
||||
Uri.parse('$_baseUrl/api/auth/register'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'username': username, 'password': password}),
|
||||
Future<List<Map<String, dynamic>>> fetchServerSms() async {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_baseUrl/api/sms?limit=999999'),
|
||||
headers: _headers,
|
||||
);
|
||||
if (res.statusCode != 201) throw Exception(_error(res));
|
||||
return jsonDecode(res.body)['token'];
|
||||
if (res.statusCode != 200) throw Exception(_error(res));
|
||||
final data = jsonDecode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data['messages']);
|
||||
}
|
||||
|
||||
Future<int> uploadSms(List<SmsMessage> messages) async {
|
||||
|
||||
76
app/lib/services/sms_foreground_handler.dart
Normal file
76
app/lib/services/sms_foreground_handler.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:telephony/telephony.dart' as tel;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/sms_message.dart';
|
||||
|
||||
class SmsForegroundHandler extends TaskHandler {
|
||||
StreamSubscription? _smsSub;
|
||||
int _uploadedCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
|
||||
_startListening();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRepeatEvent(DateTime timestamp) {}
|
||||
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp) async {
|
||||
_smsSub?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void onNotificationPressed() {
|
||||
FlutterForegroundTask.launchApp('/');
|
||||
}
|
||||
|
||||
void _startListening() async {
|
||||
final telephony = tel.Telephony.instance;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final baseUrl = prefs.getString('server_url') ?? '';
|
||||
final token = prefs.getString('token') ?? '';
|
||||
|
||||
telephony.listenIncomingSms(
|
||||
onNewMessage: (tel.SmsMessage sms) async {
|
||||
final msg = SmsMessage(
|
||||
phoneNumber: sms.address ?? '',
|
||||
contactName: sms.address,
|
||||
content: sms.body ?? '',
|
||||
type: 'received',
|
||||
smsDate: DateTime.now().toUtc().toIso8601String(),
|
||||
);
|
||||
|
||||
if (baseUrl.isNotEmpty && token.isNotEmpty) {
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('$baseUrl/api/sms/upload'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode([msg.toJson()]),
|
||||
);
|
||||
_uploadedCount++;
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationText: '已自动上传 $_uploadedCount 条短信',
|
||||
);
|
||||
} catch (_) {
|
||||
FlutterForegroundTask.updateService(
|
||||
notificationText: '上传失败,等待重试... (已上传 $_uploadedCount 条)',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
listenInBackground: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void startCallback() {
|
||||
FlutterForegroundTask.setTaskHandler(SmsForegroundHandler());
|
||||
}
|
||||
@@ -1,36 +1,60 @@
|
||||
import 'dart:async';
|
||||
import 'package:sms_maintained/sms_maintained.dart';
|
||||
import 'package:telephony/telephony.dart' as tel;
|
||||
import '../models/sms_message.dart';
|
||||
|
||||
class SmsReaderService {
|
||||
final _telephony = tel.Telephony.instance;
|
||||
|
||||
Future<List<SmsMessage>> queryAllSms() async {
|
||||
final messages = <SmsMessage>[];
|
||||
final all = await SmsQuery().querySms(
|
||||
kinds: [SmsQueryKind.Inbox, SmsQueryKind.Sent],
|
||||
|
||||
final inbox = await _telephony.getInboxSms(
|
||||
columns: [tel.SmsColumn.ADDRESS, tel.SmsColumn.BODY, tel.SmsColumn.DATE],
|
||||
);
|
||||
for (final s in all) {
|
||||
for (final s in inbox) {
|
||||
messages.add(SmsMessage(
|
||||
phoneNumber: s.address ?? '',
|
||||
contactName: s.sender ?? s.address,
|
||||
contactName: s.address,
|
||||
content: s.body ?? '',
|
||||
type: s.kind == SmsQueryKind.Sent ? 'sent' : 'received',
|
||||
smsDate: DateTime.fromMillisecondsSinceEpoch(s.date!).toUtc().toIso8601String(),
|
||||
type: 'received',
|
||||
smsDate: s.date != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(s.date!).toUtc().toIso8601String()
|
||||
: DateTime.now().toUtc().toIso8601String(),
|
||||
));
|
||||
}
|
||||
|
||||
final sent = await _telephony.getSentSms(
|
||||
columns: [tel.SmsColumn.ADDRESS, tel.SmsColumn.BODY, tel.SmsColumn.DATE],
|
||||
);
|
||||
for (final s in sent) {
|
||||
messages.add(SmsMessage(
|
||||
phoneNumber: s.address ?? '',
|
||||
contactName: s.address,
|
||||
content: s.body ?? '',
|
||||
type: 'sent',
|
||||
smsDate: s.date != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(s.date!).toUtc().toIso8601String()
|
||||
: DateTime.now().toUtc().toIso8601String(),
|
||||
));
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
Stream<SmsMessage> listenToIncoming() {
|
||||
final controller = StreamController<SmsMessage>.broadcast();
|
||||
SmsReceiver().onSmsReceived!.listen((SmsMessage sms) {
|
||||
controller.add(SmsMessage(
|
||||
phoneNumber: sms.address ?? '',
|
||||
contactName: sms.sender ?? sms.address,
|
||||
content: sms.body ?? '',
|
||||
type: 'received',
|
||||
smsDate: DateTime.now().toUtc().toIso8601String(),
|
||||
));
|
||||
});
|
||||
_telephony.listenIncomingSms(
|
||||
onNewMessage: (tel.SmsMessage sms) {
|
||||
controller.add(SmsMessage(
|
||||
phoneNumber: sms.address ?? '',
|
||||
contactName: sms.address,
|
||||
content: sms.body ?? '',
|
||||
type: 'received',
|
||||
smsDate: DateTime.now().toUtc().toIso8601String(),
|
||||
));
|
||||
},
|
||||
listenInBackground: false,
|
||||
);
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user