first commit
This commit is contained in:
60
lib/services/background_sync_service.dart
Normal file
60
lib/services/background_sync_service.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'dart:async';
|
||||
import 'time_tracking_service.dart';
|
||||
import 'statistics_service.dart';
|
||||
|
||||
/// 后台同步服务 - 定期同步应用使用数据
|
||||
class BackgroundSyncService {
|
||||
final TimeTrackingService _timeTrackingService = TimeTrackingService();
|
||||
final StatisticsService _statisticsService = StatisticsService();
|
||||
|
||||
Timer? _syncTimer;
|
||||
bool _isRunning = false;
|
||||
|
||||
/// 启动后台同步
|
||||
Future<void> start() async {
|
||||
if (_isRunning) return;
|
||||
|
||||
_isRunning = true;
|
||||
|
||||
// 立即同步一次
|
||||
await syncNow();
|
||||
|
||||
// 每 15 分钟同步一次
|
||||
_syncTimer = Timer.periodic(const Duration(minutes: 15), (timer) async {
|
||||
await syncNow();
|
||||
});
|
||||
|
||||
// 启动原生后台追踪
|
||||
await _timeTrackingService.startBackgroundTracking();
|
||||
}
|
||||
|
||||
/// 停止后台同步
|
||||
Future<void> stop() async {
|
||||
_isRunning = false;
|
||||
_syncTimer?.cancel();
|
||||
_syncTimer = null;
|
||||
|
||||
await _timeTrackingService.stopBackgroundTracking();
|
||||
}
|
||||
|
||||
/// 立即同步
|
||||
Future<void> syncNow() async {
|
||||
try {
|
||||
print('Background sync: Starting sync...');
|
||||
|
||||
// 同步今日数据
|
||||
await _timeTrackingService.syncTodayData();
|
||||
|
||||
// 刷新统计
|
||||
await _statisticsService.refreshTodayStats();
|
||||
|
||||
print('Background sync: Completed');
|
||||
} catch (e) {
|
||||
print('Background sync error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否正在运行
|
||||
bool get isRunning => _isRunning;
|
||||
}
|
||||
|
||||
183
lib/services/category_service.dart
Normal file
183
lib/services/category_service.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../database/database_helper.dart';
|
||||
|
||||
class CategoryService {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
// Web 平台使用内存存储
|
||||
final Map<String, String> _webCustomCategories = {};
|
||||
|
||||
// 预设分类规则
|
||||
static const Map<String, String> defaultCategories = {
|
||||
// 工作类
|
||||
'com.microsoft.Office.Word': 'work',
|
||||
'com.microsoft.Office.Excel': 'work',
|
||||
'com.microsoft.Office.PowerPoint': 'work',
|
||||
'com.slack': 'work',
|
||||
'com.notion.Notion': 'work',
|
||||
'com.figma.Figma': 'work',
|
||||
'com.github': 'work',
|
||||
'com.microsoft.VSCode': 'work',
|
||||
'com.jetbrains': 'work',
|
||||
'com.google.android.apps.docs': 'work',
|
||||
'com.google.android.apps.sheets': 'work',
|
||||
|
||||
// 学习类
|
||||
'com.coursera': 'study',
|
||||
'com.khanacademy': 'study',
|
||||
'com.amazon.kindle': 'study',
|
||||
'com.gingerlabs.Notability': 'study',
|
||||
'com.goodnotes': 'study',
|
||||
'com.anki': 'study',
|
||||
'com.duolingo': 'study',
|
||||
|
||||
// 娱乐类
|
||||
'com.google.YouTube': 'entertainment',
|
||||
'com.netflix.Netflix': 'entertainment',
|
||||
'com.spotify.music': 'entertainment',
|
||||
'com.spotify.client': 'entertainment',
|
||||
'com.disney': 'entertainment',
|
||||
|
||||
// 社交类
|
||||
'net.whatsapp.WhatsApp': 'social',
|
||||
'com.instagram': 'social',
|
||||
'com.twitter': 'social',
|
||||
'com.facebook.Facebook': 'social',
|
||||
'com.tencent.mm': 'social', // 微信
|
||||
'com.tencent.mobileqq': 'social', // QQ
|
||||
|
||||
// 工具类
|
||||
'com.apple.Safari': 'tool',
|
||||
'com.android.chrome': 'tool',
|
||||
'com.google.chrome': 'tool',
|
||||
'com.microsoft.edge': 'tool',
|
||||
'com.apple.mobilemail': 'tool',
|
||||
'com.google.Gmail': 'tool',
|
||||
};
|
||||
|
||||
// 获取应用分类
|
||||
Future<String> getCategory(String packageName) async {
|
||||
// 1. 先查用户自定义分类
|
||||
final customCategory = await _getCustomCategory(packageName);
|
||||
if (customCategory != null) {
|
||||
return customCategory;
|
||||
}
|
||||
|
||||
// 2. 查系统预设分类
|
||||
if (defaultCategories.containsKey(packageName)) {
|
||||
return defaultCategories[packageName]!;
|
||||
}
|
||||
|
||||
// 3. 默认分类
|
||||
return 'other';
|
||||
}
|
||||
|
||||
// 获取用户自定义分类
|
||||
Future<String?> _getCustomCategory(String packageName) async {
|
||||
// Web 平台使用内存存储
|
||||
if (kIsWeb) {
|
||||
return _webCustomCategories[packageName];
|
||||
}
|
||||
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'app_category',
|
||||
where: 'package_name = ? AND is_custom = 1',
|
||||
whereArgs: [packageName],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return maps.first['category'] as String;
|
||||
}
|
||||
|
||||
// 设置自定义分类
|
||||
Future<void> setCategory(String packageName, String category) async {
|
||||
// Web 平台使用内存存储
|
||||
if (kIsWeb) {
|
||||
_webCustomCategories[packageName] = category;
|
||||
return;
|
||||
}
|
||||
|
||||
final db = await _dbHelper.database;
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
await db.insert(
|
||||
'app_category',
|
||||
{
|
||||
'package_name': packageName,
|
||||
'category': category,
|
||||
'is_custom': 1,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
// 删除自定义分类(恢复为系统默认)
|
||||
Future<void> removeCustomCategory(String packageName) async {
|
||||
// Web 平台使用内存存储
|
||||
if (kIsWeb) {
|
||||
_webCustomCategories.remove(packageName);
|
||||
return;
|
||||
}
|
||||
|
||||
final db = await _dbHelper.database;
|
||||
await db.delete(
|
||||
'app_category',
|
||||
where: 'package_name = ? AND is_custom = 1',
|
||||
whereArgs: [packageName],
|
||||
);
|
||||
}
|
||||
|
||||
// 获取所有自定义分类
|
||||
Future<Map<String, String>> getAllCustomCategories() async {
|
||||
// Web 平台使用内存存储
|
||||
if (kIsWeb) {
|
||||
return Map<String, String>.from(_webCustomCategories);
|
||||
}
|
||||
|
||||
final db = await _dbHelper.database;
|
||||
final maps = await db.query(
|
||||
'app_category',
|
||||
where: 'is_custom = 1',
|
||||
);
|
||||
|
||||
final result = <String, String>{};
|
||||
for (final map in maps) {
|
||||
result[map['package_name'] as String] = map['category'] as String;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 批量设置分类
|
||||
Future<void> batchSetCategories(Map<String, String> categories) async {
|
||||
// Web 平台使用内存存储
|
||||
if (kIsWeb) {
|
||||
_webCustomCategories.addAll(categories);
|
||||
return;
|
||||
}
|
||||
|
||||
final db = await _dbHelper.database;
|
||||
final batch = db.batch();
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
for (final entry in categories.entries) {
|
||||
batch.insert(
|
||||
'app_category',
|
||||
{
|
||||
'package_name': entry.key,
|
||||
'category': entry.value,
|
||||
'is_custom': 1,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
}
|
||||
|
||||
156
lib/services/export_service.dart
Normal file
156
lib/services/export_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import '../database/app_usage_dao.dart';
|
||||
import '../database/daily_stats_dao.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class ExportService {
|
||||
final AppUsageDao _appUsageDao = AppUsageDao();
|
||||
final DailyStatsDao _dailyStatsDao = DailyStatsDao();
|
||||
|
||||
/// 导出 CSV 格式数据
|
||||
Future<String> exportToCSV({
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) async {
|
||||
final appUsages = await _appUsageDao.getAppUsages(
|
||||
startTime: startDate,
|
||||
endTime: endDate,
|
||||
);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// CSV 头部
|
||||
buffer.writeln('应用名称,包名,开始时间,结束时间,使用时长(秒),使用时长(格式化),分类');
|
||||
|
||||
// 数据行
|
||||
for (final usage in appUsages) {
|
||||
buffer.writeln([
|
||||
_escapeCsvField(usage.appName),
|
||||
_escapeCsvField(usage.packageName),
|
||||
usage.startTime.toIso8601String(),
|
||||
usage.endTime.toIso8601String(),
|
||||
usage.duration.toString(),
|
||||
usage.formattedDuration,
|
||||
AppTheme.getCategoryName(usage.category),
|
||||
].join(','));
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 导出统计报告(文本格式)
|
||||
Future<String> exportStatsReport({
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) async {
|
||||
final stats = await _dailyStatsDao.getStatsRange(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('=== AutoTime Tracker 统计报告 ===');
|
||||
buffer.writeln('报告时间: ${startDate.toString().split(' ')[0]} 至 ${endDate.toString().split(' ')[0]}');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('日期统计:');
|
||||
buffer.writeln('');
|
||||
|
||||
int totalWorkTime = 0;
|
||||
int totalStudyTime = 0;
|
||||
int totalEntertainmentTime = 0;
|
||||
int totalSocialTime = 0;
|
||||
int totalToolTime = 0;
|
||||
int totalTime = 0;
|
||||
|
||||
for (final stat in stats) {
|
||||
buffer.writeln('${stat.date.toString().split(' ')[0]}:');
|
||||
buffer.writeln(' 总时长: ${stat.formattedTotalTime}');
|
||||
buffer.writeln(' 工作: ${_formatTime(stat.workTime)}');
|
||||
buffer.writeln(' 学习: ${_formatTime(stat.studyTime)}');
|
||||
buffer.writeln(' 娱乐: ${_formatTime(stat.entertainmentTime)}');
|
||||
buffer.writeln(' 社交: ${_formatTime(stat.socialTime)}');
|
||||
buffer.writeln(' 工具: ${_formatTime(stat.toolTime)}');
|
||||
if (stat.efficiencyScore != null) {
|
||||
buffer.writeln(' 效率评分: ${stat.efficiencyScore}%');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
totalWorkTime += stat.workTime;
|
||||
totalStudyTime += stat.studyTime;
|
||||
totalEntertainmentTime += stat.entertainmentTime;
|
||||
totalSocialTime += stat.socialTime;
|
||||
totalToolTime += stat.toolTime;
|
||||
totalTime += stat.totalTime;
|
||||
}
|
||||
|
||||
buffer.writeln('总计:');
|
||||
buffer.writeln(' 总时长: ${_formatTime(totalTime)}');
|
||||
buffer.writeln(' 工作: ${_formatTime(totalWorkTime)} (${(totalWorkTime / totalTime * 100).toStringAsFixed(1)}%)');
|
||||
buffer.writeln(' 学习: ${_formatTime(totalStudyTime)} (${(totalStudyTime / totalTime * 100).toStringAsFixed(1)}%)');
|
||||
buffer.writeln(' 娱乐: ${_formatTime(totalEntertainmentTime)} (${(totalEntertainmentTime / totalTime * 100).toStringAsFixed(1)}%)');
|
||||
buffer.writeln(' 社交: ${_formatTime(totalSocialTime)} (${(totalSocialTime / totalTime * 100).toStringAsFixed(1)}%)');
|
||||
buffer.writeln(' 工具: ${_formatTime(totalToolTime)} (${(totalToolTime / totalTime * 100).toStringAsFixed(1)}%)');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 导出今日报告
|
||||
Future<String> exportTodayReport() async {
|
||||
final today = DateTime.now();
|
||||
final startOfDay = DateTime(today.year, today.month, today.day);
|
||||
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||
|
||||
final stats = await _dailyStatsDao.getTodayStats();
|
||||
final topApps = await _appUsageDao.getTopApps(
|
||||
startTime: startOfDay,
|
||||
endTime: endOfDay,
|
||||
limit: 10,
|
||||
);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('=== AutoTime Tracker 今日报告 ===');
|
||||
buffer.writeln('日期: ${today.toString().split(' ')[0]}');
|
||||
buffer.writeln('');
|
||||
|
||||
if (stats != null) {
|
||||
buffer.writeln('总时长: ${stats.formattedTotalTime}');
|
||||
buffer.writeln('工作: ${_formatTime(stats.workTime)}');
|
||||
buffer.writeln('学习: ${_formatTime(stats.studyTime)}');
|
||||
buffer.writeln('娱乐: ${_formatTime(stats.entertainmentTime)}');
|
||||
buffer.writeln('社交: ${_formatTime(stats.socialTime)}');
|
||||
buffer.writeln('工具: ${_formatTime(stats.toolTime)}');
|
||||
if (stats.efficiencyScore != null) {
|
||||
buffer.writeln('效率评分: ${stats.efficiencyScore}%');
|
||||
}
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
if (topApps.isNotEmpty) {
|
||||
buffer.writeln('Top 应用:');
|
||||
for (int i = 0; i < topApps.length; i++) {
|
||||
final app = topApps[i];
|
||||
buffer.writeln('${i + 1}. ${app.appName}: ${app.formattedDuration} (${AppTheme.getCategoryName(app.category)})');
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String _escapeCsvField(String field) {
|
||||
if (field.contains(',') || field.contains('"') || field.contains('\n')) {
|
||||
return '"${field.replaceAll('"', '""')}"';
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final hours = seconds ~/ 3600;
|
||||
final minutes = (seconds % 3600) ~/ 60;
|
||||
if (hours > 0) {
|
||||
return '${hours}h ${minutes}m';
|
||||
}
|
||||
return '${minutes}m';
|
||||
}
|
||||
}
|
||||
|
||||
114
lib/services/mock_data_service.dart
Normal file
114
lib/services/mock_data_service.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import '../models/daily_stats.dart';
|
||||
import '../models/app_usage.dart';
|
||||
|
||||
/// 测试数据服务 - 用于 Web 平台或开发测试
|
||||
class MockDataService {
|
||||
/// 生成今日测试统计数据
|
||||
static DailyStats generateTodayStats() {
|
||||
return DailyStats(
|
||||
date: DateTime.now(),
|
||||
totalTime: 23040, // 6小时24分钟
|
||||
workTime: 14400, // 4小时
|
||||
studyTime: 3600, // 1小时
|
||||
entertainmentTime: 3600, // 1小时
|
||||
socialTime: 1800, // 30分钟
|
||||
toolTime: 0,
|
||||
efficiencyScore: 72,
|
||||
focusScore: 65,
|
||||
appSwitchCount: 45,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 生成本周测试统计数据
|
||||
static List<DailyStats> generateWeekStats() {
|
||||
final now = DateTime.now();
|
||||
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
|
||||
|
||||
return List.generate(7, (index) {
|
||||
final date = startOfWeek.add(Duration(days: index));
|
||||
final baseTime = 20000 + (index * 500); // 每天略有不同
|
||||
|
||||
return DailyStats(
|
||||
date: date,
|
||||
totalTime: baseTime,
|
||||
workTime: (baseTime * 0.6).round(), // 60% 工作时间
|
||||
studyTime: (baseTime * 0.15).round(), // 15% 学习时间
|
||||
entertainmentTime: (baseTime * 0.15).round(), // 15% 娱乐时间
|
||||
socialTime: (baseTime * 0.08).round(), // 8% 社交时间
|
||||
toolTime: (baseTime * 0.02).round(), // 2% 工具时间
|
||||
efficiencyScore: 65 + (index * 2), // 效率评分递增
|
||||
focusScore: 60 + (index * 3),
|
||||
appSwitchCount: 40 + (index * 2),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 生成今日 Top 应用测试数据
|
||||
static List<AppUsage> generateTopApps() {
|
||||
final now = DateTime.now();
|
||||
|
||||
return [
|
||||
AppUsage(
|
||||
packageName: 'com.google.chrome',
|
||||
appName: 'Chrome',
|
||||
startTime: now.subtract(const Duration(hours: 2, minutes: 15)),
|
||||
endTime: now,
|
||||
duration: 8100, // 2小时15分钟
|
||||
category: 'work',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
AppUsage(
|
||||
packageName: 'com.microsoft.vscode',
|
||||
appName: 'VS Code',
|
||||
startTime: now.subtract(const Duration(hours: 1, minutes: 30)),
|
||||
endTime: now,
|
||||
duration: 5400, // 1小时30分钟
|
||||
category: 'work',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
AppUsage(
|
||||
packageName: 'com.slack',
|
||||
appName: 'Slack',
|
||||
startTime: now.subtract(const Duration(hours: 1)),
|
||||
endTime: now,
|
||||
duration: 3600, // 1小时
|
||||
category: 'work',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
AppUsage(
|
||||
packageName: 'com.notion',
|
||||
appName: 'Notion',
|
||||
startTime: now.subtract(const Duration(minutes: 45)),
|
||||
endTime: now,
|
||||
duration: 2700, // 45分钟
|
||||
category: 'work',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
AppUsage(
|
||||
packageName: 'com.apple.mail',
|
||||
appName: 'Mail',
|
||||
startTime: now.subtract(const Duration(minutes: 30)),
|
||||
endTime: now,
|
||||
duration: 1800, // 30分钟
|
||||
category: 'work',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// 检查是否应该使用测试数据
|
||||
static bool shouldUseMockData() {
|
||||
return kIsWeb;
|
||||
}
|
||||
}
|
||||
|
||||
199
lib/services/statistics_service.dart
Normal file
199
lib/services/statistics_service.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import '../models/daily_stats.dart';
|
||||
import '../models/app_usage.dart';
|
||||
import '../database/app_usage_dao.dart';
|
||||
import '../database/daily_stats_dao.dart';
|
||||
import 'mock_data_service.dart';
|
||||
|
||||
class StatisticsService {
|
||||
final AppUsageDao _appUsageDao = AppUsageDao();
|
||||
final DailyStatsDao _dailyStatsDao = DailyStatsDao();
|
||||
|
||||
// 获取今日统计(如果不存在则计算)
|
||||
Future<DailyStats> getTodayStats() async {
|
||||
// Web 平台返回测试数据
|
||||
if (kIsWeb) {
|
||||
return MockDataService.generateTodayStats();
|
||||
}
|
||||
|
||||
final today = DateTime.now();
|
||||
|
||||
// 先查数据库
|
||||
var stats = await _dailyStatsDao.getTodayStats();
|
||||
if (stats != null) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
// 如果不存在,计算并保存
|
||||
stats = await _calculateDailyStats(today);
|
||||
if (stats != null) {
|
||||
await _dailyStatsDao.upsertDailyStats(stats);
|
||||
} else {
|
||||
// 如果没有数据,返回空统计
|
||||
stats = DailyStats(
|
||||
date: today,
|
||||
totalTime: 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// 计算指定日期的统计
|
||||
Future<DailyStats?> _calculateDailyStats(DateTime date) async {
|
||||
final startOfDay = DateTime(date.year, date.month, date.day);
|
||||
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||
|
||||
// 获取该日期的所有应用使用记录
|
||||
final appUsages = await _appUsageDao.getAppUsages(
|
||||
startTime: startOfDay,
|
||||
endTime: endOfDay,
|
||||
);
|
||||
|
||||
if (appUsages.isEmpty) return null;
|
||||
|
||||
// 按分类聚合
|
||||
final categoryTime = <String, int>{};
|
||||
int totalTime = 0;
|
||||
int appSwitchCount = 0;
|
||||
|
||||
for (final usage in appUsages) {
|
||||
categoryTime[usage.category] = (categoryTime[usage.category] ?? 0) + usage.duration;
|
||||
totalTime += usage.duration;
|
||||
appSwitchCount += usage.deviceUnlockCount;
|
||||
}
|
||||
|
||||
// 计算效率评分
|
||||
final efficiencyScore = _calculateEfficiencyScore(categoryTime, totalTime);
|
||||
|
||||
// 计算专注度评分
|
||||
final focusScore = _calculateFocusScore(appSwitchCount, totalTime);
|
||||
|
||||
return DailyStats(
|
||||
date: startOfDay,
|
||||
totalTime: totalTime,
|
||||
workTime: categoryTime['work'] ?? 0,
|
||||
studyTime: categoryTime['study'] ?? 0,
|
||||
entertainmentTime: categoryTime['entertainment'] ?? 0,
|
||||
socialTime: categoryTime['social'] ?? 0,
|
||||
toolTime: categoryTime['tool'] ?? 0,
|
||||
efficiencyScore: efficiencyScore,
|
||||
focusScore: focusScore,
|
||||
appSwitchCount: appSwitchCount,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// 计算效率评分
|
||||
int _calculateEfficiencyScore(Map<String, int> categoryTime, int totalTime) {
|
||||
if (totalTime == 0) return 0;
|
||||
|
||||
// 工作时间占比(40%)
|
||||
final workRatio = (categoryTime['work'] ?? 0) / totalTime;
|
||||
final workScore = workRatio * 40;
|
||||
|
||||
// 学习时间占比(30%)
|
||||
final studyRatio = (categoryTime['study'] ?? 0) / totalTime;
|
||||
final studyScore = studyRatio * 30;
|
||||
|
||||
// 娱乐时间占比(越低越好,30%)
|
||||
final entertainmentRatio = (categoryTime['entertainment'] ?? 0) / totalTime;
|
||||
final entertainmentScore = (1 - entertainmentRatio) * 30;
|
||||
|
||||
return (workScore + studyScore + entertainmentScore).round();
|
||||
}
|
||||
|
||||
// 计算专注度评分
|
||||
int _calculateFocusScore(int appSwitchCount, int totalTime) {
|
||||
if (totalTime == 0) return 0;
|
||||
|
||||
// 平均每小时切换次数
|
||||
final switchesPerHour = (appSwitchCount / (totalTime / 3600));
|
||||
|
||||
// 理想情况:每小时切换 < 10 次 = 100分
|
||||
// 每小时切换 > 50 次 = 0分
|
||||
final score = 100 - (switchesPerHour * 2).clamp(0, 100);
|
||||
|
||||
return score.round();
|
||||
}
|
||||
|
||||
// 获取本周统计
|
||||
Future<List<DailyStats>> getWeekStats() async {
|
||||
// Web 平台返回测试数据
|
||||
if (kIsWeb) {
|
||||
return MockDataService.generateWeekStats();
|
||||
}
|
||||
|
||||
final stats = await _dailyStatsDao.getWeekStats();
|
||||
|
||||
// 如果数据不完整,计算缺失的日期
|
||||
final now = DateTime.now();
|
||||
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
|
||||
|
||||
final result = <DailyStats>[];
|
||||
for (int i = 0; i < 7; i++) {
|
||||
final date = startOfWeek.add(Duration(days: i));
|
||||
final existing = stats.firstWhere(
|
||||
(s) => s.date.year == date.year &&
|
||||
s.date.month == date.month &&
|
||||
s.date.day == date.day,
|
||||
orElse: () => DailyStats(
|
||||
date: date,
|
||||
totalTime: 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
result.add(existing);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取本月统计
|
||||
Future<List<DailyStats>> getMonthStats() async {
|
||||
return await _dailyStatsDao.getMonthStats();
|
||||
}
|
||||
|
||||
// 刷新今日统计(重新计算)
|
||||
Future<DailyStats> refreshTodayStats() async {
|
||||
final today = DateTime.now();
|
||||
final stats = await _calculateDailyStats(today);
|
||||
|
||||
if (stats != null) {
|
||||
await _dailyStatsDao.upsertDailyStats(stats);
|
||||
return stats;
|
||||
} else {
|
||||
final emptyStats = DailyStats(
|
||||
date: today,
|
||||
totalTime: 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
await _dailyStatsDao.upsertDailyStats(emptyStats);
|
||||
return emptyStats;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Top 应用
|
||||
Future<List<AppUsage>> getTopApps({
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
// Web 平台返回测试数据
|
||||
if (kIsWeb) {
|
||||
return MockDataService.generateTopApps().take(limit).toList();
|
||||
}
|
||||
|
||||
return await _appUsageDao.getTopApps(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
182
lib/services/time_tracking_service.dart
Normal file
182
lib/services/time_tracking_service.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/app_usage.dart';
|
||||
import '../database/app_usage_dao.dart';
|
||||
import 'category_service.dart';
|
||||
import 'statistics_service.dart';
|
||||
|
||||
class TimeTrackingService {
|
||||
static const MethodChannel _channel = MethodChannel('autotime_tracker/time_tracking');
|
||||
|
||||
final AppUsageDao _appUsageDao = AppUsageDao();
|
||||
final CategoryService _categoryService = CategoryService();
|
||||
final StatisticsService _statisticsService = StatisticsService();
|
||||
|
||||
/// 检查权限状态
|
||||
Future<bool> hasPermission() async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('hasPermission');
|
||||
return result ?? false;
|
||||
} catch (e) {
|
||||
print('Error checking permission: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求权限
|
||||
Future<bool> requestPermission() async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('requestPermission');
|
||||
return result ?? false;
|
||||
} catch (e) {
|
||||
print('Error requesting permission: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取应用使用数据(从系统 API)
|
||||
Future<List<AppUsageData>> getAppUsageFromSystem({
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod<List<dynamic>>('getAppUsage', {
|
||||
'startTime': startTime.millisecondsSinceEpoch,
|
||||
'endTime': endTime.millisecondsSinceEpoch,
|
||||
});
|
||||
|
||||
if (result == null) return [];
|
||||
|
||||
return result
|
||||
.map((item) => AppUsageData.fromMap(Map<String, dynamic>.from(item)))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error getting app usage from system: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步应用使用数据到数据库
|
||||
Future<void> syncAppUsageToDatabase({
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
}) async {
|
||||
try {
|
||||
// 1. 从系统获取数据
|
||||
final systemData = await getAppUsageFromSystem(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
);
|
||||
|
||||
if (systemData.isEmpty) {
|
||||
print('No app usage data from system');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 转换为 AppUsage 模型并分类
|
||||
final appUsages = <AppUsage>[];
|
||||
for (final data in systemData) {
|
||||
// 获取分类
|
||||
final category = await _categoryService.getCategory(data.packageName);
|
||||
|
||||
// 创建 AppUsage 对象
|
||||
final usage = AppUsage(
|
||||
packageName: data.packageName,
|
||||
appName: data.appName,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
duration: data.duration,
|
||||
category: category,
|
||||
deviceUnlockCount: data.deviceUnlockCount ?? 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
appUsages.add(usage);
|
||||
}
|
||||
|
||||
// 3. 批量插入数据库
|
||||
if (appUsages.isNotEmpty) {
|
||||
await _appUsageDao.batchInsertAppUsages(appUsages);
|
||||
print('Synced ${appUsages.length} app usage records to database');
|
||||
}
|
||||
|
||||
// 4. 更新今日统计
|
||||
await _statisticsService.refreshTodayStats();
|
||||
} catch (e) {
|
||||
print('Error syncing app usage to database: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步今日数据
|
||||
Future<void> syncTodayData() async {
|
||||
final now = DateTime.now();
|
||||
final startOfDay = DateTime(now.year, now.month, now.day);
|
||||
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||
|
||||
await syncAppUsageToDatabase(
|
||||
startTime: startOfDay,
|
||||
endTime: endOfDay,
|
||||
);
|
||||
}
|
||||
|
||||
/// 启动后台追踪
|
||||
Future<void> startBackgroundTracking() async {
|
||||
try {
|
||||
await _channel.invokeMethod('startBackgroundTracking');
|
||||
} catch (e) {
|
||||
print('Error starting background tracking: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止后台追踪
|
||||
Future<void> stopBackgroundTracking() async {
|
||||
try {
|
||||
await _channel.invokeMethod('stopBackgroundTracking');
|
||||
} catch (e) {
|
||||
print('Error stopping background tracking: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查后台追踪状态
|
||||
Future<bool> isBackgroundTrackingActive() async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('isBackgroundTrackingActive');
|
||||
return result ?? false;
|
||||
} catch (e) {
|
||||
print('Error checking background tracking status: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 系统返回的应用使用数据模型
|
||||
class AppUsageData {
|
||||
final String packageName;
|
||||
final String appName;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final int duration; // 秒
|
||||
final int? deviceUnlockCount;
|
||||
|
||||
AppUsageData({
|
||||
required this.packageName,
|
||||
required this.appName,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.duration,
|
||||
this.deviceUnlockCount,
|
||||
});
|
||||
|
||||
factory AppUsageData.fromMap(Map<String, dynamic> map) {
|
||||
return AppUsageData(
|
||||
packageName: map['packageName'] as String,
|
||||
appName: map['appName'] as String,
|
||||
startTime: DateTime.fromMillisecondsSinceEpoch(map['startTime'] as int),
|
||||
endTime: DateTime.fromMillisecondsSinceEpoch(map['endTime'] as int),
|
||||
duration: map['duration'] as int,
|
||||
deviceUnlockCount: map['deviceUnlockCount'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user