Commit da29459c6a41307422ba8489db6ac9db57a5f389

Authored by 权海
1 parent b73c5efd

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

... ... @@ -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 AppPlatformHostApi _platformHostApi = AppPlatformHostApi();
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(
... ...
... ... @@ -18,7 +18,7 @@ typedef AppPigeonPlatformProvider = AppPigeonPlatform Function();
AppPigeonPlatform resolveAppPigeonPlatform({TargetPlatform? targetPlatform}) {
final platform = targetPlatform ?? defaultTargetPlatform;
if (platform == TargetPlatform.ohos) {
if (platform.name == 'ohos') {
return AppPigeonPlatform.ohos;
}
if (platform == TargetPlatform.iOS) {
... ...
... ... @@ -192,26 +192,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();
}
... ... @@ -1350,7 +1346,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 =
... ... @@ -1359,7 +1354,6 @@ class AppleHealthRawDataCoreService {
}
Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async {
if (!_isDebug) return;
try {
await _rawDataApi.saveNativeCallFlutterChange(
relativeDataTypes: dataTypes ?? const <int>[],
... ... @@ -1370,7 +1364,6 @@ class AppleHealthRawDataCoreService {
}
Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async {
if (!_isDebug) return;
try {
await _rawDataApi.saveFlutterCaculateFinished(
relativeDataTypes: dataTypes,
... ... @@ -1383,7 +1376,6 @@ class AppleHealthRawDataCoreService {
Future<void> _saveLocalNotificationDebugEvent(
Map<String, Object?> event,
) async {
if (!_isDebug) return;
try {
await _localNotificationDebugStore.append(event);
await _saveNativeLocalNotificationDebugEvent(event);
... ... @@ -2058,7 +2050,7 @@ class HealthRawStressLocalStore {
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 7,
version: 8,
onCreate: (db, version) async {
await _createTables(db);
},
... ... @@ -2082,6 +2074,9 @@ class HealthRawStressLocalStore {
if (oldVersion < 7) {
await _createSleepResultsTable(db);
}
if (oldVersion < 8) {
await _addUpdateTimeColumns(db);
}
},
),
);
... ... @@ -2107,7 +2102,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('''
... ... @@ -2122,7 +2118,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);
... ... @@ -2138,7 +2135,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
)
''');
}
... ... @@ -2154,11 +2152,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',
... ... @@ -2320,6 +2336,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'baseline_resting_hr': point.baselineRestingHr,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2333,6 +2350,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'source_end_time': point.sourceEndTime,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2345,6 +2363,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'state': point.state.value,
'data_time': point.dataTime,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2359,6 +2378,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'awak_minutes': result.awakMinutes,
'sleep_minutes': result.sleepMinutes,
'uploaded': result.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
};
}
... ... @@ -2389,6 +2409,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(
... ... @@ -2419,6 +2445,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']],
... ... @@ -2458,6 +2485,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']],
... ... @@ -2503,12 +2531,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 '暂无本地推送诊断日志';
... ...
... ... @@ -181,10 +181,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
version: "0.3.5+5"
crypto:
dependency: "direct main"
description:
... ...
... ... @@ -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 {
... ... @@ -1185,7 +1250,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) {
... ... @@ -1196,6 +1266,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
... ... @@ -1232,6 +1304,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));
... ... @@ -1255,6 +1330,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));
... ... @@ -1287,6 +1365,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));
... ... @@ -1303,6 +1383,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));
... ... @@ -1630,6 +1712,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,
};
}
... ... @@ -1646,6 +1729,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,
};
}
... ... @@ -1658,6 +1742,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,
};
}
... ... @@ -1672,9 +1757,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;
... ...