Commit 4c43a3d4b23566b79627ba30998ec9459b804188

Authored by 权海
1 parent ac583fa7

feat(ui):开放线上隐藏的日志入口

(cherry picked from commit da29459c)

# Conflicts:
#	lib/core/platform/pigeon_api_facade.dart
#	pubspec.lock
... ... @@ -31,10 +31,8 @@ class DeveloperOptionsItem {
enum DeveloperOptionsAction {
shareHealthRawDataDatabase,
shareHealthRawUploadLog,
shareNativeAppleHealthObserverRecord,
shareFlutterObserverRecord,
shareUploadTaskRecord,
shareLocalNotificationDebugRecord,
sendTestHrvLocalNotification,
clearHealthRawDataDatabaseAndUploadLog,
... ... @@ -52,61 +50,84 @@ class DeveloperOptionsController extends GetxController {
final PlatformHostApi _platformHostApi = PlatformHostApi();
final TextEditingController tokenController = TextEditingController();
final List<DeveloperOptionsItem> options = const [
DeveloperOptionsItem(
title: 'User Information',
routeName: Routes.USER_INFORMATION,
),
DeveloperOptionsItem(
title: 'Route List',
routeName: Routes.ROUTE_LIST,
),
DeveloperOptionsItem(
title: 'Token Login',
routeName: Routes.TOKEN_LOGIN,
),
DeveloperOptionsItem(
title: 'Share HealthRawData Database',
action: DeveloperOptionsAction.shareHealthRawDataDatabase,
),
DeveloperOptionsItem(
title: '本地上传日志',
action: DeveloperOptionsAction.shareHealthRawUploadLog,
),
DeveloperOptionsItem(
title: '分享原生AppleHealthObserver记录',
action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
),
DeveloperOptionsItem(
title: '分享Flutter Observer-Caculate记录',
action: DeveloperOptionsAction.shareFlutterObserverRecord,
),
DeveloperOptionsItem(
title: '分享上传Task日志',
action: DeveloperOptionsAction.shareUploadTaskRecord,
),
DeveloperOptionsItem(
title: '分享本地推送诊断日志',
action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
),
DeveloperOptionsItem(
title: '测试本地推送 HRV',
action: DeveloperOptionsAction.sendTestHrvLocalNotification,
),
DeveloperOptionsItem(
title: '清除本地数据库、上传日志',
action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
),
DeveloperOptionsItem(
title: 'Global Env',
action: DeveloperOptionsAction.globalEnv,
),
];
late final List<DeveloperOptionsItem> options;
@override
void onInit() {
super.onInit();
tokenController.text = _userPreferencesStorage.accessToken;
final config = Get.find<AppEnvironmentConfig>();
if (config.isDebug) {
options = const [
DeveloperOptionsItem(
title: 'User Information',
routeName: Routes.USER_INFORMATION,
),
DeveloperOptionsItem(
title: 'Route List',
routeName: Routes.ROUTE_LIST,
),
DeveloperOptionsItem(
title: 'Token Login',
routeName: Routes.TOKEN_LOGIN,
),
DeveloperOptionsItem(
title: '分享本地计算数据库文件',
action: DeveloperOptionsAction.shareHealthRawDataDatabase,
),
DeveloperOptionsItem(
title: '分享原生响应AppleHealth记录',
action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
),
DeveloperOptionsItem(
title: '分享Flutter 响应AppleHealth回调记录',
action: DeveloperOptionsAction.shareFlutterObserverRecord,
),
DeveloperOptionsItem(
title: '分享本地推送诊断日志',
action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
),
DeveloperOptionsItem(
title: '测试本地推送 HRV',
action: DeveloperOptionsAction.sendTestHrvLocalNotification,
),
DeveloperOptionsItem(
title: '清除本地数据库、上传日志',
action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
),
DeveloperOptionsItem(
title: 'Global Env',
action: DeveloperOptionsAction.globalEnv,
),
];
} else {
options = const [
DeveloperOptionsItem(
title: 'User Information',
routeName: Routes.USER_INFORMATION,
),
DeveloperOptionsItem(
title: '分享本地计算数据库文件',
action: DeveloperOptionsAction.shareHealthRawDataDatabase,
),
DeveloperOptionsItem(
title: '分享原生响应AppleHealth记录',
action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
),
DeveloperOptionsItem(
title: '分享Flutter 响应AppleHealth回调记录',
action: DeveloperOptionsAction.shareFlutterObserverRecord,
),
DeveloperOptionsItem(
title: '分享本地推送诊断日志',
action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
),
DeveloperOptionsItem(
title: '清除本地数据库、上传日志',
action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
),
];
}
}
Future<void> optionItemOnTap(DeveloperOptionsItem item) async {
... ... @@ -120,18 +141,12 @@ class DeveloperOptionsController extends GetxController {
case DeveloperOptionsAction.shareHealthRawDataDatabase:
await shareHealthRawDataDatabase();
break;
case DeveloperOptionsAction.shareHealthRawUploadLog:
await shareHealthRawUploadLog();
break;
case DeveloperOptionsAction.shareNativeAppleHealthObserverRecord:
await shareNativeAppleHealthObserverRecord();
break;
case DeveloperOptionsAction.shareFlutterObserverRecord:
await shareFlutterObserverRecord();
break;
case DeveloperOptionsAction.shareUploadTaskRecord:
await shareUploadTaskRecord();
break;
case DeveloperOptionsAction.shareLocalNotificationDebugRecord:
await shareLocalNotificationDebugRecord();
break;
... ...
... ... @@ -15,6 +15,9 @@ import '../models/watch_theme_models.dart';
class WatchThemeController extends GetxController {
WatchThemeController(this._themeApi, this._vipApi);
static const int _developerOptionsTapCount = 8;
static const Duration _developerOptionsTapWindow = Duration(seconds: 3);
final ThemeApi _themeApi;
final VipApi _vipApi;
... ... @@ -23,6 +26,8 @@ class WatchThemeController extends GetxController {
final officialThemeItems = <WatchThemeItem>[].obs;
final customThemeItems = <WatchThemeItem>[].obs;
final selectedThemeItem = Rx<WatchThemeItem?>(null);
int _headerTapCount = 0;
DateTime? _firstHeaderTapAt;
@override
void onInit() {
... ... @@ -63,6 +68,26 @@ class WatchThemeController extends GetxController {
Get.back();
}
void onHeaderTap() {
final now = DateTime.now();
final firstTapAt = _firstHeaderTapAt;
if (firstTapAt == null ||
now.difference(firstTapAt) > _developerOptionsTapWindow) {
_firstHeaderTapAt = now;
_headerTapCount = 1;
return;
}
_headerTapCount += 1;
if (_headerTapCount < _developerOptionsTapCount) {
return;
}
_firstHeaderTapAt = null;
_headerTapCount = 0;
Get.toNamed(Routes.DEVELOPER_OPTIONS);
}
Future<void> selectOfficialTheme(int index) async {
await Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: {
'theme': officialThemeItems[index],
... ...
... ... @@ -23,9 +23,13 @@ class WatchThemeView extends GetView<WatchThemeController> {
padding: EdgeInsets.only(top: topPadding, bottom: 24),
child: Column(
children: [
WatchThemeHeader(
themeImageUrl:
controller.selectedThemeItem.value?.energeticImage,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: controller.onHeaderTap,
child: WatchThemeHeader(
themeImageUrl:
controller.selectedThemeItem.value?.energeticImage,
),
),
const SizedBox(height: 26),
OfficialThemeGrid(
... ...
... ... @@ -193,26 +193,22 @@ class AppleHealthRawDataCoreService {
}
Future<bool> shareNativeAppleHealthObserverRecord() async {
if (!_isDebug) return false;
return _rawDataApi.shareNativeAppleHealthObserverRecord();
}
Future<bool> shareFlutterObserverRecord() async {
if (!_isDebug) return false;
return _rawDataApi.shareFlutterObserverRecord();
}
Future<bool> shareUploadTaskRecord() async {
if (!_isDebug) return false;
return _rawDataApi.shareUploadTaskRecord();
}
Future<String> readLocalNotificationDebugLogText() {
return _localNotificationDebugStore.readText(isDebug: _isDebug);
return _localNotificationDebugStore.readText();
}
Future<bool> shareLocalNotificationDebugRecord() async {
if (!_isDebug) return false;
return _shareNativeLocalNotificationDebugRecord();
}
... ... @@ -1477,7 +1473,6 @@ class AppleHealthRawDataCoreService {
required String apiName,
required Object? result,
}) async {
if (!_isDebug) return;
final file = await _uploadApiLogFile();
await file.parent.create(recursive: true);
final line =
... ... @@ -1486,7 +1481,6 @@ class AppleHealthRawDataCoreService {
}
Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async {
if (!_isDebug) return;
try {
await _rawDataApi.saveNativeCallFlutterChange(
relativeDataTypes: dataTypes ?? const <int>[],
... ... @@ -1497,7 +1491,6 @@ class AppleHealthRawDataCoreService {
}
Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async {
if (!_isDebug) return;
try {
await _rawDataApi.saveFlutterCaculateFinished(
relativeDataTypes: dataTypes,
... ... @@ -1510,7 +1503,6 @@ class AppleHealthRawDataCoreService {
Future<void> _saveLocalNotificationDebugEvent(
Map<String, Object?> event,
) async {
if (!_isDebug) return;
try {
await _localNotificationDebugStore.append(event);
await _saveNativeLocalNotificationDebugEvent(event);
... ... @@ -2229,7 +2221,7 @@ class HealthRawStressLocalStore {
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 7,
version: 8,
onCreate: (db, version) async {
await _createTables(db);
},
... ... @@ -2253,6 +2245,9 @@ class HealthRawStressLocalStore {
if (oldVersion < 7) {
await _createSleepResultsTable(db);
}
if (oldVersion < 8) {
await _addUpdateTimeColumns(db);
}
},
),
);
... ... @@ -2278,7 +2273,8 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
is_workout_recovery INTEGER NOT NULL DEFAULT 0,
is_sleep_likely INTEGER NOT NULL DEFAULT 0,
is_suspected_activity INTEGER NOT NULL DEFAULT 0,
uploaded INTEGER NOT NULL DEFAULT 0
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
)
''');
await db.execute('''
... ... @@ -2293,7 +2289,8 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
is_workout_recovery INTEGER NOT NULL DEFAULT 0,
is_sleep_likely INTEGER NOT NULL DEFAULT 0,
is_suspected_activity INTEGER NOT NULL DEFAULT 0,
uploaded INTEGER NOT NULL DEFAULT 0
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
)
''');
await _createDailyStressTable(db);
... ... @@ -2309,7 +2306,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
stress_score INTEGER NOT NULL,
state INTEGER NOT NULL,
data_time INTEGER NOT NULL,
uploaded INTEGER NOT NULL DEFAULT 0
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
)
''');
}
... ... @@ -2325,11 +2323,29 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
in_bed_minutes INTEGER NOT NULL,
awak_minutes INTEGER NOT NULL,
sleep_minutes INTEGER NOT NULL,
uploaded INTEGER NOT NULL DEFAULT 0
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
)
''');
}
Future<void> _addUpdateTimeColumns(DatabaseExecutor db) async {
for (final table in const [
hrvResultsTable,
realtimeStressResultsTable,
dailyStressResultsTable,
sleepResultsTable,
]) {
try {
await db.execute(
'ALTER TABLE $table ADD COLUMN update_time INTEGER NOT NULL DEFAULT 0',
);
} on DatabaseException catch (error) {
if (!error.isDuplicateColumnError()) rethrow;
}
}
}
Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
for (final column in const [
'is_workout',
... ... @@ -2491,6 +2507,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'baseline_resting_hr': point.baselineRestingHr,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2504,6 +2521,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'source_end_time': point.sourceEndTime,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2516,6 +2534,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'state': point.state.value,
'data_time': point.dataTime,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2530,6 +2549,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'awak_minutes': result.awakMinutes,
'sleep_minutes': result.sleepMinutes,
'uploaded': result.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2560,6 +2580,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
final existingRow = existing.first;
if (_matchesStoredValues(
existingRow, row, _rawResultStoredValueKeys(row))) {
await db.update(
table,
<String, Object?>{'update_time': row['update_time']},
where: 'raw_end_time = ?',
whereArgs: [row['raw_end_time']],
);
return;
}
final uploadPayloadChanged = !_matchesStoredValues(
... ... @@ -2590,6 +2616,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'is_sleep_likely': row['is_sleep_likely'],
'is_suspected_activity': row['is_suspected_activity'],
'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
'update_time': row['update_time'],
},
where: 'raw_end_time = ?',
whereArgs: [row['raw_end_time']],
... ... @@ -2629,6 +2656,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'state': row['state'],
'data_time': row['data_time'],
'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
'update_time': row['update_time'],
},
where: 'date = ?',
whereArgs: [row['date']],
... ... @@ -2674,12 +2702,17 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'awak_minutes': row['awak_minutes'],
'sleep_minutes': row['sleep_minutes'],
'uploaded': valueChanged ? 0 : existingRow['uploaded'],
'update_time': row['update_time'],
},
where: 'date = ?',
whereArgs: [row['date']],
);
}
int _currentUnixSeconds() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
return <String>[
'user_id',
... ...
... ... @@ -35,10 +35,7 @@ class HealthRawLocalNotificationDebugStore {
);
}
Future<String> readText({required bool isDebug}) async {
if (!isDebug) {
return '当前非 Debug 模式,未记录本地推送诊断日志';
}
Future<String> readText() async {
final file = await _file();
if (!await file.exists()) {
return '暂无本地推送诊断日志';
... ...
... ... @@ -391,6 +391,71 @@ void main() {
expect(daily.single.uploaded, isTrue);
});
test('local result rows include update time and refresh on upsert', () async {
const base = 1800000000;
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: [_realtimeStressPoint(base + 20)],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
await store.upsertDailyStressPoints(
userId: 42,
points: [
HealthRawDailyStressPoint(
userId: 42,
date: 20260101,
stressValue: 40,
stressScore: 40,
state: HealthRawStressState.normal,
dataTime: base + 20,
),
],
);
await store.upsertSleepResults(
userId: 42,
results: const [
HealthRawSleepResult(
userId: 42,
date: base + 30,
startDate: base,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 480,
awakMinutes: 10,
sleepMinutes: 470,
),
],
);
final firstRows = await store.debugQueryAllRows(42);
final firstHrvUpdateTime = firstRows.hrvRows.single['update_time'] as int;
expect(firstHrvUpdateTime, greaterThan(0));
expect(firstRows.realtimeRows.single['update_time'], greaterThan(0));
expect(firstRows.dailyStressRows.single['update_time'], greaterThan(0));
expect(firstRows.sleepRows.single['update_time'], greaterThan(0));
await store.markHrvStressUploaded(rawEndTimes: [base + 10], userId: 42);
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final secondRows = await store.debugQueryAllRows(42);
expect(
secondRows.hrvRows.single['update_time'],
greaterThan(firstHrvUpdateTime),
);
expect(secondRows.hrvRows.single['uploaded'], 1);
});
test(
'startCoreCaculate skips raw reads when health auth and local data are missing',
() async {
... ... @@ -1266,7 +1331,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {};
final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
final Map<int, List<HealthRawSleepResult>> _sleep = {};
final Map<int, Map<int, int>> _hrvUpdateTimes = {};
final Map<int, Map<int, int>> _realtimeUpdateTimes = {};
final Map<int, Map<int, int>> _dailyUpdateTimes = {};
final Map<int, Map<int, int>> _sleepUpdateTimes = {};
final List<String> operationLog;
var _updateTimeClock = 1;
var realtimeNotificationWindowQueryCount = 0;
void insertDailyStress(HealthRawDailyStressPoint point) {
... ... @@ -1277,6 +1347,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
byDate[point.date] = point;
_daily[point.userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
_dailyUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
}
@override
... ... @@ -1313,6 +1385,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
flags: point.flags,
uploaded: same ? existing.uploaded : false,
);
_hrvUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.rawEndTime] =
_nextUpdateTime();
}
_hrv[result.userId] = hrvByTime.values.toList()
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
... ... @@ -1336,6 +1411,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
flags: point.flags,
uploaded: same ? existing.uploaded : false,
);
_realtimeUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.rawEndTime] =
_nextUpdateTime();
}
_realtime[result.userId] = realtimeByTime.values.toList()
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
... ... @@ -1368,6 +1446,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
dataTime: point.dataTime,
uploaded: isToday ? false : (same ? existing.uploaded : false),
);
_dailyUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
}
_daily[userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
... ... @@ -1384,6 +1464,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
};
for (final result in results) {
byDate[result.date] = result;
_sleepUpdateTimes.putIfAbsent(
result.userId, () => <int, int>{})[result.date] = _nextUpdateTime();
}
_sleep[userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
... ... @@ -1711,6 +1793,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _hrvUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
};
}
... ... @@ -1727,6 +1810,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _realtimeUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
};
}
... ... @@ -1739,6 +1823,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
'state': point.state.value,
'data_time': point.dataTime,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _dailyUpdateTimes[point.userId]?[point.date] ?? 0,
};
}
... ... @@ -1753,9 +1838,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
'awak_minutes': result.awakMinutes,
'sleep_minutes': result.sleepMinutes,
'uploaded': result.uploaded ? 1 : 0,
'update_time': _sleepUpdateTimes[result.userId]?[result.date] ?? 0,
};
}
int _nextUpdateTime() => _updateTimeClock++;
int _todayDateKey() {
final now = DateTime.now();
return now.year * 10000 + now.month * 100 + now.day;
... ...